1<?php
2/**
3 * Plugin imagereference
4 *
5 * Syntax: <imgref linkname> - creates a figure link to an image
6 *         <imgcaption linkname <orientation> | Image caption> Image/Table</imgcaption>
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Martin Heinemann <martinheinemann@tudor.lu>
10 * @author     Gerrit Uitslag <klapinklapin@gmail.com>
11 */
12
13/**
14 * All DokuWiki plugins to extend the parser/rendering mechanism
15 * need to inherit from this class
16 */
17class syntax_plugin_imagereference_imgcaption extends DokuWiki_Syntax_Plugin {
18
19    /* @var array $captionParam */
20    protected $captionParam = array();
21
22    /**
23     * @return string Syntax type
24     */
25    public function getType() {
26        return 'formatting';
27    }
28
29    /**
30     * @return string Paragraph type
31     */
32    public function getPType() {
33        return 'normal';
34    }
35
36    /**
37     * @return int Sort order
38     */
39    public function getSort() {
40        return 196;
41    }
42
43    /**
44     * Specify modes allowed in the imgcaption/tabcaption
45     * Using getAllowedTypes() includes too much modes.
46     *
47     * @param string $mode Parser mode
48     * @return bool true if $mode is accepted
49     */
50    public function accepts($mode) {
51        $allowedsinglemodes = array(
52            'media', //allowed content
53            'internallink', 'externallink', 'linebreak', //clickable img allowed
54            'emaillink', 'windowssharelink', 'filelink',
55            'plugin_graphviz', 'plugin_ditaa'    //plugins
56        );
57        if(in_array($mode, $allowedsinglemodes)) return true;
58
59        return parent::accepts($mode);
60    }
61
62    /**
63     * Connect lookup pattern to lexer.
64     *
65     * @param string $mode Parser mode
66     */
67    public function connectTo($mode) {
68        $this->Lexer->addEntryPattern('<imgcaption.*?>(?=.*?</imgcaption>)', $mode, 'plugin_imagereference_imgcaption');
69
70    }
71
72    public function postConnect() {
73        $this->Lexer->addExitPattern('</imgcaption>', 'plugin_imagereference_imgcaption');
74    }
75
76    /**
77     * Handle matches of the imgcaption/tabcaption syntax
78     *
79     * @param string          $match The match of the syntax
80     * @param int             $state The state of the handler
81     * @param int             $pos The position in the document
82     * @param Doku_Handler    $handler The handler
83     * @return array Data for the renderer
84     */
85    public function handle($match, $state, $pos, Doku_Handler $handler) {
86        global $ACT;
87        switch($state) {
88            case DOKU_LEXER_ENTER :
89                $rawparam = trim(substr($match, 1, -1));
90                $param    = $this->parseParam($rawparam);
91
92                //store parameters for closing tag
93                $this->captionParam = $param;
94
95                //local counter for preview
96                if($ACT == 'preview') {
97                    self::captionReferencesStorage($param['type'], $param);
98                }
99
100                return array('caption_open', $param);
101
102            case DOKU_LEXER_UNMATCHED :
103                // drop unmatched text inside imgcaption/tabcaption tag
104                return array('data', '');
105
106            // when normal text it's usefull, then use next lines instead
107            //$handler->_addCall('cdata', array($match), $pos);
108            //return false;
109
110            case DOKU_LEXER_EXIT :
111                //load parameters
112                $param = $this->captionParam;
113                return array('caption_close', $param);
114        }
115
116        return array();
117    }
118
119    /**
120     * Render xhtml output, latex output or metadata
121     *
122     * @param string         $mode      Renderer mode (supported modes: xhtml, latex and metadata)
123     * @param Doku_Renderer  $renderer  The renderer
124     * @param array          $indata    The data from the handler function
125     * @return bool If rendering was successful.
126     */
127    public function render($mode, Doku_Renderer $renderer, $indata) {
128        global $ID, $ACT;
129        list($case, $data) = $indata;
130
131        switch($mode) {
132            case 'xhtml' :
133                /** @var Doku_Renderer_xhtml $renderer */
134                switch($case) {
135                    case 'caption_open' :
136                        $renderer->doc .= $this->captionStart($data);
137                        return true;
138
139                    // $data is empty string
140                    case 'data' :
141                        $renderer->doc .= $data;
142                        return true;
143
144                    case 'caption_close' :
145                        //determine referencenumber
146                        if($ACT == 'preview') {
147                            $caprefs = self::getCaptionreferences($ID, $data['type']);
148                        } else {
149                            $caprefs = p_get_metadata($ID, 'captionreferences '.$data['type']);
150                        }
151                        $data['refnumber'] = array_search($data['caprefname'], $caprefs);
152
153                        if(!$data['refnumber']) {
154                            $data['refnumber'] = "##";
155                        }
156
157                        $renderer->doc .= $this->captionEnd($data);
158                        return true;
159                }
160                break;
161
162            case 'metadata' :
163                /** @var Doku_Renderer_metadata $renderer */
164                switch($case) {
165                    case 'caption_open' :
166                        // store the image refences as metadata to expose them to the imgref/tabref and undercaption renderer
167                        //create array and add index zero entry, so stored caprefnames start counting on one.
168                        $type = $data['type'];
169                        if(!isset($renderer->meta['captionreferences'][$type])) {
170                            $renderer->meta['captionreferences'][$type][] = '';
171                        }
172                        $renderer->meta['captionreferences'][$type][] = $data['caprefname'];
173
174                        //abstract
175                        if($renderer->capture && $data['caption']) $renderer->doc .= '<';
176                        return true;
177
178                    case 'caption_close' :
179                        //abstract
180                        if($renderer->capture && $data['caption']) $renderer->doc .= hsc($data['caption']).'>';
181                        return true;
182                }
183                break;
184
185            case 'latex' :
186                if($data['type'] == 'img') {
187                    $floattype = 'figure';
188                } else {
189                    $floattype = 'table';
190                }
191                switch($case) {
192                    case 'caption_open' :
193                        $orientation = "\\centering";
194                        if(strpos($data['classes'], 'left') !== false) {
195                            $orientation = "\\left";
196                        } elseif(strpos($data['classes'], 'right') !== false) {
197                            $orientation = "\\right";
198                        }
199                        $renderer->doc .= "\\begin{".$floattype."}[!h]{".$orientation."}";
200                        return true;
201
202                    case 'data' :
203                        $renderer->doc .= trim($data);
204                        return true;
205
206                    case 'caption_close' :
207                        $renderer->doc .= "\\caption{".$data['caption']."}\\label{".$data['caprefname']."}\\end{".$floattype."}";
208                        return true;
209                }
210                break;
211        }
212        return false;
213    }
214
215    /**
216     * When a array of caption data is given, this is stored. Otherwise the array is returned
217     *
218     * @param string $type          'img' or 'tab'
219     * @param array  $captiondata   array with data of the caption
220     * @param string $id            page id
221     * @return void|array
222     */
223    static private function captionReferencesStorage($type, $captiondata = null, $id = null) {
224        global $ID;
225        static $captionreferences = array();
226
227        if($captiondata !== null) {
228            //store reference names
229            if(!isset($captionreferences[$ID][$type])) {
230                $captionreferences[$ID][$type][] = '';
231            }
232            $captionreferences[$ID][$type][] = $captiondata['caprefname'];
233            return null;
234
235        } else {
236            //return reference names
237            if($id === null) {
238                $id = $ID;
239            }
240            return $captionreferences[$id][$type];
241        }
242    }
243
244    /**
245     * Returns the captionreferences of page
246     *
247     * @param string $id   page id
248     * @param string $type caption type 'img' or 'tab'
249     * @return array of stored reference names
250     */
251    static public function getCaptionreferences($id, $type) {
252        return self::captionReferencesStorage($type, null, $id);
253    }
254
255    /**
256     * Parse parameters part of <imgcaption imgref class1 class2|Caption of image>
257     *
258     * @param string $str space separated parameters e.g."imgref class1 class2|Caption of image"
259     * @return array(string imgrefname, string classes, string caption)
260     */
261    protected function parseParam($str) {
262        if(empty($str)) {
263            return array();
264        }
265
266        // get caption, second part
267        $parsed  = explode("|", $str, 2);
268        $caption = '';
269        if(isset($parsed[1])) $caption = trim($parsed[1]);
270
271        // get the img ref name. Its the first word
272        $parsed     = explode(" ", $parsed[0], 3);
273        $captiontype = substr($parsed[0], 0, 3);
274        $caprefname = $parsed[1];
275
276        $tokens  = preg_split('/\s+/', $parsed[2], 9); // limit is defensive
277        $classes = '';
278        foreach($tokens as $token) {
279            // restrict token (class names) characters to prevent any malicious data
280            if(preg_match('/[^A-Za-z0-9_-]/', $token)) continue;
281            $token = trim($token);
282            if($token == '') continue;
283            $classes .= ' '.$token;
284        }
285
286        return array(
287            'caprefname'  => $caprefname,
288            'classes'     => $classes,
289            'caption'     => $caption,
290            'type' => $captiontype
291        );
292    }
293
294    /**
295     * @var string $captionStart opening tag of caption, image/table dependent
296     * @var string $captionEnd closing tag of caption, image/table dependent
297     */
298    protected $captionStart = '<span id="%s" class="imgcaption%s">';
299    protected $captionEnd   = '</span>';
300
301    /**
302     * Create html of opening of caption wrapper
303     *
304     * @param array $data(caprefname, classes, ..)
305     * @return string html start of caption wrapper
306     */
307    protected function captionStart($data) {
308      return sprintf(
309          $this->captionStart,
310          $data['type'].'_'.cleanID($data['caprefname']),
311          $data['classes'],
312          (strpos($data['classes'], 'center') == false ? '':' center')  //needed for tabcaptionbox
313      );
314    }
315
316    /**
317     * Create html of closing of caption wrapper
318     *
319     * @param array $data(caprefname, refnumber, caption, ..) Caption data
320     * @return string html caption wrapper
321     */
322    protected function captionEnd($data) {
323        return '<span class="undercaption">'
324                    .$this->getLang($data['type'].'short').' '.$data['refnumber']
325                    .($data['caption'] ? ': ' . hsc($data['caption']) : '')
326                    .'<a href=" "><span></span></a>'
327                .'</span>'
328            . $this->captionEnd;
329    }
330}
331