1<?php
2/**
3 * CommandEmbedding -- Base class for command embedding syntaxes.
4 *
5 * A command embedding is a type of syntax plugin whose syntax includes
6 * a compliant call string and optional content.  A call string has the
7 * following syntax, expressed in W3C-style BNF:
8 *
9 *   call_string  ::= command_name ('?' params)?
10 *   command_name ::= name
11 *   params       ::= param ('&' param)*
12 *   param        ::= value | name '=' value?
13 *   name         ::= [a-zA-Z] [a-zA-Z0-9_]*
14 *   value        ::= [a-zA-Z0-9_\-.]+
15 *
16 * This class is a subclass of DokuWiki_Syntax_Plugin.  It implements handle()
17 * and render() on behalf of the subclass, but the subclass is responsible for
18 * implementing all other required methods of DokuWiki_Syntax_Plugin.
19 *
20 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
21 * @author     Joe Lapp <http://www.spiderjoe.com>
22 *
23 * Modification history:
24 *
25 * 8/26/05 - Reverted back to 'substition' to permit recursion. JTL
26 * 8/29/05 - Added $renderer parameter to runCommand(). JTL
27 * 8/29/05 - Fixed failure to load extension for pre-cached data. JTL
28 */
29
30//// CONSTANTS ////////////////////////////////////////////////////////////////
31
32require_once(DOKU_PLUGIN.'syntax.php');
33
34if(!defined('COMMANDPLUGIN_DEFS'))
35{
36    define('COMMANDPLUGIN_EXT_PATH', DOKU_PLUGIN.'command/ext/');
37
38    define('COMMANDPLUGIN_LEX_CMD', '[a-zA-Z][a-zA-Z0-9_]*');
39    define('COMMANDPLUGIN_LEX_CMD_PARAMS', // lexer prohibits subpatterns
40        COMMANDPLUGIN_LEX_CMD.'\?[a-zA-Z0-9_.\-=&]*');
41
42    $TMP_NAME = '[a-zA-Z][a-zA-Z0-9_\-]*';
43    $TMP_VCHAR = '[a-zA-Z0-9_.\-]';
44    $TMP_PARAM = '('.$TMP_VCHAR.'+|'.$TMP_NAME.'\='.$TMP_VCHAR.'*)';
45    define('COMMANDPLUGIN_CALL_PREG',
46        '/^('.$TMP_NAME.')(\?'.$TMP_PARAM.'(&'.$TMP_PARAM.')*)?$/');
47
48    define('COMMANDPLUGIN_NOT_FOUND', '##_COMMAND_NOT_FOUND_##');
49    define('COMMANDPLUGIN_INVALID_SYNTAX', '##_INVALID_COMMAND_SYNTAX_##');
50
51    define('COMMANDPLUGIN_DEFS', '*');
52}
53
54/******************************************************************************
55  CommandEmbedding
56******************************************************************************/
57
58class CommandEmbedding extends DokuWiki_Syntax_Plugin
59{
60    //// POLYMORPHIC METHODS //////////////////////////////////////////////////
61
62    /**
63     * getEmbeddingType() returns a string that uniquely identifies the
64     * particular command embedding and ideally also suggests the behavior
65     * of the command embedding.  This string is passed to the command
66     * extensions to allow them to behave in part according to the type of
67     * embedding the command was called from.  A command typically only
68     * uses this information if it outputs text for inclusion on the page.
69     *
70     * Each subclass must override and implement this method.
71     */
72
73    function getEmbeddingType()
74    {
75        return null; // subclass must return non-empty string
76    }
77
78    /**
79     * parseCommandSyntax() takes a string that contains the entire text of
80     * the command and parses out the call string and the content.  It returns
81     * an array of the form array(callString, content).  This function cannot
82     * return an error; it is presumed that if the lexer matched the syntax,
83     * then the embedding is able to extract the call string and the content.
84     *
85     * Each subclass must override and implement this method.
86     */
87
88    function parseCommandSyntax($match)
89    {
90        return null; // subclass must return array(callString, content)
91    }
92
93    //// PUBLIC METHODS ///////////////////////////////////////////////////////
94
95    function handle($match, $state, $pos, &$handler)
96    {
97        // Parse the command.
98
99        $parse = $this->parseCommandSyntax($match);
100        list($callString, $content) = $parse;
101
102        $call = $this->parseCommandCall($callString);
103        if($call == null)
104            return array($state, array('=', COMMANDPLUGIN_INVALID_SYNTAX));
105
106        $cmdName = $call[0];
107        $params = $call[1];
108        $paramHash = $call[2];
109
110        // Load the command extension.
111
112        if(!$this->loadExtension($cmdName))
113            return array($state, array('=', COMMANDPLUGIN_NOT_FOUND));
114
115        // Invoke the command.
116
117        $embedding = $this->getEmbeddingType();
118        $methodObj = $this->getMethodObject($cmdName, 'getCachedData',
119                      '$p, $pH, $c, &$e', '"'.$embedding.'", $p, $pH, $c, $e');
120        $errMsg = '';
121        $cachedData = $methodObj($params, $paramHash, $content, $errMsg);
122        if(!empty($errMsg))
123            return array($state, array('=', '##'.$errMsg.'##'));
124
125        // Return successful results.
126
127        return array($state, array($cmdName, $cachedData));
128    }
129
130    function render($mode, &$renderer, $data)
131    {
132        if($mode == 'xhtml')
133        {
134            // Extract cached data.
135
136            list($state, $match) = $data;
137            list($cmdName, $cachedData) = $match;
138
139            // Output data raw, as when command not found or error.
140
141            if($cmdName == '=')
142                $renderer->doc .= htmlspecialchars($cachedData);
143
144            // Run the command with the cached data.
145
146            else
147            {
148                if(!$this->loadExtension($cmdName))
149                {
150                    $renderer->doc .= COMMANDPLUGIN_NOT_FOUND;
151                    return true;
152                }
153
154                $embedding = $this->getEmbeddingType();
155                $methodObj = $this->getMethodObject($cmdName, 'runCommand',
156                                '$c, &$r, &$e', '"'.$embedding.'", $c, $r, $e');
157                $errMsg = '';
158                $output = $methodObj($cachedData, $renderer, $errMsg);
159                if(!empty($errMsg))
160                    $renderer->doc .= htmlspecialchars($errMsg);
161                else if($output !== null)
162                    $renderer->doc .= $output;
163            }
164            return true;
165        }
166        return false;
167    }
168
169    //// PRIVATE METHODS //////////////////////////////////////////////////////
170
171    /**
172     * parseCommandCall() validates and parses the call string portion of a
173     * command.  This string includes the command name and all of its
174     * parameters, but excludes the content and the bounding syntax.
175     *
176     * Returns an array of the form array(cmd, params, hash):
177     *
178     * cmd: The name of the command in lowercase.
179     *
180     * params: An array of parameters, indexed by their order of occurrence
181     * in the parameter list.  If the parameter was an assignment (using '='),
182     * the element will be an array of the form array(name, value).  If the
183     * parameter was a simple value (no '='), the element will be a string
184     * containing that value.
185     *
186     * hash: An associative array indexed by parameter name (for assignment
187     * parameters) and by parameter value (for simple value parameters).  If
188     * no value was assigned to a parameter, or if the parameter is itself
189     * just a value, the value for the key is an empty string.  For example,
190     * the call string "do?1.5&1.5&a&b=&c=2" produces array('1.5' => '',
191     * 'a' => '', 'b' => '', 'c' => '1').
192     *
193     * If there are no parameters, the method returns array(cmd, null, null).
194     */
195
196    function parseCommandCall($callString)
197    {
198        $matches = array();
199        if(!preg_match(COMMANDPLUGIN_CALL_PREG, $callString, $matches))
200            return null; // syntax error
201
202        $cmdName = strtolower($matches[1]);
203        if(sizeof($matches) == 2)
204            return array($cmdName, array(), array()); // no parameters
205
206        $rawParams = explode('&', substr($matches[2], 1));
207        $params = array();
208        $paramHash = array();
209
210        foreach($rawParams as $rawParam)
211        {
212            $equalsPos = strpos($rawParam, '=');
213            if($equalsPos === false)
214            {
215                $params[] = $rawParam;
216                $paramHash[$rawParam] = '';
217            }
218            else
219            {
220                $paramName = substr($rawParam, 0, $equalsPos);
221                // next line works even if '=' is last char
222                $paramValue = substr($rawParam, $equalsPos + 1);
223                $params[] = array($paramName, $paramValue);
224                $paramHash[$paramName] = $paramValue;
225            }
226        }
227        return array($cmdName, $params, $paramHash);
228    }
229
230    /**
231     * loadExtension() loads the command extension.
232     */
233
234    function loadExtension($cmdName)
235    {
236        $extFile = COMMANDPLUGIN_EXT_PATH.$cmdName.'.php';
237        if(!file_exists($extFile)) // PHP caches the results, fortunately
238            return false;
239
240        require_once($extFile);
241        return true;
242    }
243
244    /**
245     * getMethodObject() returns a method object that the caller may use to
246     * invoke a method.  The method object is cached as a function of
247     * command name and method name so that multiple calls use the same
248     * method object, saving clock cycles.
249     */
250
251    function &getMethodObject($cmdName, $methodName, $methodSig, $callArgs)
252    {
253        static $methodObjects = array();
254
255        $key = $cmdName.'*'.$methodName;
256        if(isset($methodObjects[$key]))
257            return $methodObjects[$key];
258
259        $className = 'CommandPluginExtension_'.$cmdName;
260        $methodObj = create_function($methodSig,
261                       'return '.$className.'::'.$methodName.'('.$callArgs.');');
262        $methodObjects[$key] =& $methodObj;
263        return $methodObj;
264    }
265}
266
267?>