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                }
189                else if ($data['type'] == 'ggb') {
190                    // no latex support for GeoGebra applets
191                    return true;
192                } else {
193                    $floattype = 'table';
194                }
195                switch($case) {
196                    case 'caption_open' :
197                        $orientation = "\\centering";
198                        if(strpos($data['classes'], 'left') !== false) {
199                            $orientation = "\\left";
200                        } elseif(strpos($data['classes'], 'right') !== false) {
201                            $orientation = "\\right";
202                        }
203                        $renderer->doc .= "\\begin{".$floattype."}[!h]{".$orientation."}";
204                        return true;
205
206                    case 'data' :
207                        $renderer->doc .= trim($data);
208                        return true;
209
210                    case 'caption_close' :
211                        $renderer->doc .= "\\caption{".$data['caption']."}\\label{".$data['caprefname']."}\\end{".$floattype."}";
212                        return true;
213                }
214                break;
215        }
216        return false;
217    }
218
219    /**
220     * When a array of caption data is given, this is stored. Otherwise the array is returned
221     *
222     * @param string $type          'img' or 'tab'
223     * @param array  $captiondata   array with data of the caption
224     * @param string $id            page id
225     * @return void|array
226     */
227    static private function captionReferencesStorage($type, $captiondata = null, $id = null) {
228        global $ID;
229        static $captionreferences = array();
230
231        if($captiondata !== null) {
232            //store reference names
233            if(!isset($captionreferences[$ID][$type])) {
234                $captionreferences[$ID][$type][] = '';
235            }
236            $captionreferences[$ID][$type][] = $captiondata['caprefname'];
237            return null;
238
239        } else {
240            //return reference names
241            if($id === null) {
242                $id = $ID;
243            }
244            return $captionreferences[$id][$type];
245        }
246    }
247
248    /**
249     * Returns the captionreferences of page
250     *
251     * @param string $id   page id
252     * @param string $type caption type 'img' or 'tab'
253     * @return array of stored reference names
254     */
255    static public function getCaptionreferences($id, $type) {
256        return self::captionReferencesStorage($type, null, $id);
257    }
258
259    /**
260     * Parse parameters part of <imgcaption imgref class1 class2|Caption of image>
261     *
262     * @param string $str space separated parameters e.g."imgref class1 class2|Caption of image"
263     * @return array(string imgrefname, string classes, string caption)
264     */
265    protected function parseParam($str) {
266        if(empty($str)) {
267            return array();
268        }
269
270        // get caption, second part
271        $parsed  = explode("|", $str, 2);
272        $caption = '';
273        if(isset($parsed[1])) $caption = trim($parsed[1]);
274
275        // get the img ref name. Its the first word
276        $parsed     = array_pad(explode(" ", $parsed[0], 3), 3, null);
277        $captiontype = substr($parsed[0], 0, 3);
278        $caprefname = $parsed[1];
279
280        $tokens  = preg_split('/\s+/', $parsed[2], 9); // limit is defensive
281        $classes = '';
282        foreach($tokens as $token) {
283            // restrict token (class names) characters to prevent any malicious data
284            if(preg_match('/[^A-Za-z0-9_-]/', $token)) continue;
285            $token = trim($token);
286            if($token == '') continue;
287            $classes .= ' '.$token;
288        }
289
290        return array(
291            'caprefname'  => $caprefname,
292            'classes'     => $classes,
293            'caption'     => $caption,
294            'type' => $captiontype
295        );
296    }
297
298    /**
299     * @var string $captionStart opening tag of caption, image/table dependent
300     * @var string $captionEnd closing tag of caption, image/table dependent
301     */
302    protected $captionStart = '<span id="%s" class="imgcaption%s">';
303    protected $captionEnd   = '</span>';
304
305    /**
306     * Create html of opening of caption wrapper
307     *
308     * @param array $data(caprefname, classes, ..)
309     * @return string html start of caption wrapper
310     */
311    protected function captionStart($data) {
312      return sprintf(
313          $this->captionStart,
314          $data['type'].'_'.cleanID($data['caprefname']),
315          $data['classes'],
316          (strpos($data['classes'], 'center') == false ? '':' center')  //needed for tabcaptionbox
317      );
318    }
319
320    /**
321     * Create html of closing of caption wrapper
322     *
323     * @param array $data(caprefname, refnumber, caption, ..) Caption data
324     * @return string html caption wrapper
325     */
326    protected function captionEnd($data) {
327        return '<span class="undercaption">'
328                    .$this->getLang($data['type'].'short').' '.$data['refnumber']
329                    .($data['caption'] ? ': ' . hsc($data['caption']) : '')
330                    .'<a href=" "><span></span></a>'
331                .'</span>'
332            . $this->captionEnd;
333    }
334}
335