<?php /** * PyCode plugin: it embeds a Python script hosted in a remote repository. * * syntax.php: it defines all the methods used by PyCode plugin * who extend DokuWiki's syntax. * * @author Torpedo <dgtorpedo@gmail.com> * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @package syntax */ if (!defined("DOKU_INC")) die(); // the plugin must be run within Dokuwiki require_once "method.php"; // common methods used by PyCode plugin /** * This class defines all the methods used by the PyCode plugin to produce * the plugin's output. * * It extends DokuWiki's basic syntax defined in lib/plugins/syntax.php. * * @package syntax_pycode */ class syntax_plugin_pycode extends DokuWiki_Syntax_Plugin { /** * Constructor method for class suitable for any initialization. */ public function __construct() { $this->mpp = new method_pycode_plugin; } /** * Define the syntax types that this plugin applies when founds * its token: it is simply replaced. * * @return (str) */ public function getType() { return "substition"; } /** * Define how the plugin's output is handled regarding paragraphs. * * Open paragraphs will be closed before plugin output and * the plugin output will not starts with a paragraph: * <p>foo</p> * <pycode> * <p>bar</p> * * @return (str) */ public function getPType() { return "block"; } /** * Define the priority used to determine in which order modes are added: * the mode with the lowest sort number will win. * * Since this plugin provides text codes and internal links, it is * sorted at: * Doku_Parser_Mode_code (= 200) < PyCode plugin * Doku_Parser_Mode_internallink (= 310) < PyCode plugin * * @return (int) */ public function getSort() { return 315; } /** * Define the regular expression needed to match the plugin's syntax. * * This plugin use the following general syntax: * <pycode [list of parameters]> * * @param (str) $mode name for the format mode of the final output * (at the present only $mode == "xhtml" is supported) */ public function connectTo($mode) { $this->Lexer->addSpecialPattern("<pycode.*?>", $mode, "plugin_pycode"); } /** * Handle the plugin's syntax matched. * * This method is called every time the parser encounters the * plugin's syntax in order to produce a list of instructions for the * renderer, which will be interpreted later. * * @param (str) $match the text matched * @param (int) $state the lexer state * @param (int) $pos the character position of the matched text * @param (obj) $handler object reference to the Doku_Handler class, * defined in inc/parser/handler.php * @return (arr) $data it contains the parameters handed to the * plugin's syntax and found in the wiki page, that is: * array { * [0] => (str) <src-url> * [1] => (str) <flag-c|f|l> * [2] => (str) <name-class|function|lines> * [3] => (str) <name-class> * } * where only element [0] is obligatory so the following * are valid combinations: * <src-url> * <src-url> c <name-class> * <src-url> f <name-function> <name-class> * <src-url> f <name-function> * <src-url> l <name-lines> */ public function handle($match, $state, $pos, Doku_Handler $handler) { $data = array(); $match = $this->mpp->_remove_multi_space($match); $match = substr($match, 8, -1); // strips "<pycode " and ">" // get user option for line numbers $usr_nums = null; $re = "/(-nums)(\s*)(=)(\s*)(0|1)/"; preg_match($re, $match, $matches); if (preg_match($re, $match, $matches) == 1) { $usr_nums = $matches[5]; } // get user option for title $usr_title = null; $re = "/(-title)(\s*)(=)(\s*)(\'|\")(.*)(\'|\")/"; preg_match($re, $match, $matches); if (preg_match($re, $match, $matches) == 1) { $usr_title = $matches[6]; } // get user option for docstrings $usr_docstr = null; $re = "/(-docstr)(\s*)(=)(\s*)(0|1)/"; preg_match($re, $match, $matches); if (preg_match($re, $match, $matches) == 1) { $usr_docstr = $matches[5]; } // remove user options and create data array $re = "/(\s*-nums.*|\s*-title.*|\s*-docstr.*)/"; $subst = ""; $match = preg_replace($re, $subst, $match); $data = explode(" ", $match); // append user options $data["usr_nums"] = $usr_nums; $data["usr_title"] = $usr_title; $data["usr_docstr"] = $usr_docstr; return $data; } /** * Process the list of instructions that render the final output. * * It's used an array to remember if, in the same wiki page, * the same <file> is called one or more times, in order to do only one * connection to the relative repository to download its code. * As soon as the content of <file> is downloaded, it's stored in a local * directory and used to render the output, even for the oher calls * of <file>. * If, visiting another wiki page, the same <file> is called again or * every time the page is refreshed, this array is unset (desired effect) * so, before download the code again, it's checked how much * up-to-date is the local copy of <file> against the original copy * in the repository. * The array is of the form: * array { * [<src-url>] => (int) 0|1|2 * } * with the following meaning: * <src-url> = 0: <file> has been just downloaded so it's up-to-date * <src-url> = 1: <file> has been downloaded in the current session * and can be reused in the same page * <src-url> = 2: <file> was downloaded in a previous session so * it could be out-of-date * * Since is saved a copy of <file>, it is necessary to know when it's no * longer used in any wiki page. * Hence it's used a logfile in which are recorded, for each wiki page, all * the repository used. When all the references to the same repository are * deleted from all wiki pages, the relative local copy of <file> is * deleted too. * * @param (str) $mode name for the format mode of the final output * (at the present only $mode == "xhtml" is supported) * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) * @param (arr) $data it contains the parameters handled to the * plugin's syntax and found in the wiki page, that is: * array { * [0] => (str) <src-url> * [1] => (str) <flag-c|f|l> * [2] => (str) <name-class|function|lines> * [3] => (str) <name-class> * ["usr_nums"] => (str) -nums = 0|1 * ["usr_title"] => (str) -title = "<new-title>" * ["usr-docstr"] => (str) -docstr = 0|1 * } * where only element [0] is obligatory and element [3] is * the name of the class at wich the method belongs to * so the following are valid combinations: * <src-url> * <src-url> [c <name-class> [-docstr]] * <src-url> [f <name-function> [<name-class>] [-nums] [-title]] * <src-url> [l <name-lines> [-nums] [-title]] */ public function render($mode, Doku_Renderer $renderer, $data) { global $INPUT; $repo = array(); // store all <src-url>(s) found in the current page $code_error = array("error", "notfound-lns", "notfound-def", "notfound-cls"); // eventualy disable the cache $cache = $this->getConf("cache"); if ($cache == 1){ $renderer->nocache(); } try { // check if url to the source code is right $src_url = $data[0]; $src_url = $this->mpp->_check_src_url($src_url); if (in_array($src_url, $code_error)) { throw new Exception($this->mpp->_check_src_url($src_url)); } // get info associated to the code $flag = $data[1]; $name = $data[2]; $subname = $data[3]; $nums = $data["usr_nums"]; $title = $data["usr_title"]; $docstr = $data["usr_docstr"]; list($name_host, $name_repo, $name_brch, $name_file) = $this->mpp->_get_names($src_url); $loc_url = $this->mpp->_get_loc_url($name_host, $name_repo, $name_brch, $name_file); $lang = $this->mpp->_get_lang($name_file); $raw_url = $this->mpp->_get_raw_url($src_url); // check if in this session <file> has been alredy downloaded if ((file_exists($loc_url) == false)) { $repo[$src_url] = 0; } elseif (file_exists($loc_url) == true and in_array($src_url, array_keys($repo)) == false) { $repo[$src_url] = 2; } // get the code if ($flag == "" and $name != "") { throw new Exception("wrong-flag"); } elseif ($flag != "" and $name == "") { throw new Exception("wrong-flag"); } elseif (($flag == "f" or $flag == "c") and $lang != "python") { throw new Exception("wrong-flag"); } if ($repo[$src_url] == 0) { // get the whole code list($code_raw, $sl_raw, $el_raw) = $this->mpp->_get_code($raw_url); if (in_array($code_raw, $code_error)) { throw new Exception($code_raw); } // save in local and use this instead of the repo $this->mpp->_save_code($loc_url, $code_raw); } elseif ($repo[$src_url] == 2) { list($code_raw, $sl_raw, $el_raw) = $this->mpp->_get_code($raw_url, $flag, $name, $subname); if (in_array($code_raw, $code_error)) { throw new Exception($code_raw); } if ($flag == "c") { // temporaly save in local and use this instead of the repo // because to chek if a class is changed is necessary to // check if all its methods are changed too $tmp_raw_loc_url = $loc_url . ".tmp"; $this->mpp->_save_code($tmp_raw_loc_url, $code_raw); } } list($code_loc, $sl_loc, $el_loc) = $this->mpp->_get_code($loc_url, $flag, $name, $subname); if (in_array($code_loc, $code_error)) { throw new Exception($code_loc); } // call the printer if ($repo[$src_url] == 0 or $repo[$src_url] == 1) { if (in_array($flag, array("", "l", "f"))) { $range = range($sl_loc, $el_loc); $this->_print_code($renderer, $src_url, $code_loc, $lang, $range, $flag, $name, $subname, $nums, $title); } elseif ($flag == "c") { $tree_loc = $this->mpp->_get_tree($code_loc); $this->_print_tree($renderer, $loc_url, $tree_loc, $docstr); } $repo[$src_url] = 1; } elseif ($repo[$src_url] == 2) { $code_dif = $this->mpp->_get_code_dif($code_raw, $code_loc); if (in_array($flag, array("", "l", "f"))) { if ($code_dif == "dif") { $this->_print_note($renderer, $loc_url, $code_raw, $sl_loc, $el_loc); $range = range($sl_loc, $el_loc); } else { $range = range($sl_raw, $el_raw); // sth. is changed outside the range } $this->_print_code($renderer, $src_url, $code_loc, $lang, $range, $flag, $name, $subname, $nums, $title); } elseif ($flag == "c") { $tree_loc = $this->mpp->_get_tree($code_loc); if ($code_dif == "dif") { $tree_raw = $this->mpp->_get_tree($code_raw); $tree_dif = $this->mpp->_get_tree_dif($tmp_raw_loc_url, $loc_url, $tree_raw, $tree_loc); $this->_print_note($renderer, $loc_url, $code_raw, $sl_loc, $el_loc); $this->_print_tree($renderer, $loc_url, $tree_dif, $docstr); } else { $this->_print_tree($renderer, $loc_url, $tree_loc, $docstr); } unlink($tmp_raw_loc_url); } } // record the <file>(s) embedded in this wiki page $tmp_log_url = DOKU_PLUGIN . "pycode/tmp/logfile.tmp"; $this->mpp->_save_code($tmp_log_url, $loc_url); } catch (Exception $error) { if ($error->getMessage() == "error") { $this->_print_error($renderer); } elseif ($error->getMessage() == "wrong-flag") { $this->_print_wrong_flag($renderer); } elseif ($error->getMessage() == "notfound-lns") { $this->_print_notfound_lns($renderer); } elseif ($error->getMessage() == "notfound-def") { $this->_print_notfound_def($renderer); } elseif ($error->getMessage() == "notfound-cls") { $this->_print_notfound_cls($renderer); } } } /** * Print the code of <file>. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) * @param (str) $src_url the url to the source code of <file> in the repo * Bitbucket <src-url> = * "https://bitbucket.org/<user>/<repo>/src/<branch>/<file>" * GitHub <src-url> = * "https://github.com/<user>/<repo>/blob/<branch>/<file>" * @param (arr) $code the code to embed * @param (str) $lang the language name used in <file> * @param (arr) $range the numbers of the lines matching the code to embed * @param (str) $flag flag to indicate what kind of code we want to embed: * "" = all | "c" = class | "f" = function | "l" = lines * @param (str) $name if specified, it can be only: * name-function|lines * @param (str) $subname if specified, it can be only: * name-class * @param (int) $nums if specified, it can be only: * 0 = hide line numbers | 1 = show line numbers * @param (str) $title if specified, it is the new user's title for the * code embedded */ private function _print_code(Doku_Renderer $renderer, $src_url, $code, $lang, $range, $flag, $name = null, $subname = null, $nums = null, $title = null) { if ($flag == "f") { $code = $this->mpp->_remove_indent($code); } $code = implode($code, PHP_EOL); if ($nums === null) { $nums = $this->getConf("nums"); } // make the title for the code if ($title === null) { $title = $this->getConf("title"); if ($title != "none") { if ($title == "file â‹… class â‹… def â‹… #:#") { $src = basename($src_url) . " â‹… "; } elseif ($title == "src-url â‹… class â‹… def â‹… #:#") { $src = $src_url . " â‹… "; } if ($flag == "") { $class = ""; $def = ""; } elseif ($flag == "f") { if ($subname === null) { $class = ""; $def = "def: " . $name . " â‹… "; } else { $class = "class: " . $subname . " â‹… "; $def = "def: " . $name . " â‹… "; } } elseif ($flag == "l") { $class = ""; $def = ""; } $lns = "Ln: " . reset($range) . "-" . end($range); $title = "$src" . "$class" . "$def" . "$lns"; } } else { if ($title == "") { $title = "none"; } } // print if ($nums == 1) { $renderer->doc .= "<div class='nums_pycode'>"; $renderer->doc .= "<dl class='code nums_pycode'>"; if ($title != "none") { $renderer->doc .= "<dt> </dt>"; } $renderer->doc .= "<dd>"; $renderer->doc .= "<pre class='code nums_pycode'>"; foreach ($range as $number) { if (strlen(end($range)) <= 4) { $number = str_pad($number, 4, " ", STR_PAD_LEFT); } else { $number = str_pad($number, 5, " ", STR_PAD_LEFT); } $renderer->doc .= "$number<br>"; } $renderer->doc .= "</pre> </dd> </dl> </div>"; $renderer->doc .= "<div class='code_pycode'>"; } if ($title != "none") { $icon = $this->mpp->_get_icon($src_url); $renderer->doc .= "<dl class='code code_pycode'>"; $renderer->doc .= "<dt><img class='code_pycode' src='" . $icon . "'>$title</dt>"; $renderer->doc .= "<dd>"; } $renderer->doc .= "<pre class='code $lang'>"; $renderer->doc .= $this->mpp->_get_geshi_code($code, $lang); $renderer->doc .= "</pre>"; if ($title != "none") { $renderer->doc .= "</dd> </dl>"; } if ($nums == 1) { $renderer->doc .= "</div>"; } } /** * Print the tree structure of a given class defined in <file>. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) * @param (str) $loc_url the url to the raw code of the copy of <file> in * <srv-path>/lib/plugins/pycode/tmp/<host>/<repo>/<branch>/<file> * @param (arr) $tree the name of a class and its methods, that is: * array { * [0] => array { * [0] => (str) "class <name-class>(<parameters>)" * [1] => (str) <name-class> * [2] => (int) <#> * } * [i] => array { * [0] => (str) "class <name-function>(<parameters>)" * [1] => (str) <name-function> * [2] => (int) <#> * } * } * where <#> is a flag used by the print function of the tree * structure of the given class and which tells "how" to render * each name; it can takes the following values: * <#> = 0 means "unchanged" * <#> = 1 means "changed" * <#> = 2 means "deleted" * <#> = 3 means "added" * @param (int) $docstr if specified, it can be only: * 0 = hide docstring | 1 = show docstring */ private function _print_tree(Doku_Renderer $renderer, $loc_url, $tree, $docstr = null) { global $INFO; $namespace = $INFO["namespace"]; if ($docstr === null) { $docstr = $this->getConf("docstr"); } // get the descriptive part of the docstring if ($docstr == 1) { $all_brief = array(); foreach ($tree as $key => $val) { if ($key == 0) { list($code, $sl, $el) = $this->mpp->_get_code($loc_url, "c", $val[1]); if ($code != "notfound-cls") { $brief = $this->mpp->_get_docstr($code, $val[1], "c"); } } else { list($code, $sl, $el) = $this->mpp->_get_code($loc_url, "f", $val[1], $tree[0][1]); if ($code != "notfound-def") { $brief = $this->mpp->_get_docstr($code, $val[1], "f"); } } array_push($all_brief, $brief); } } // this is what we want to produce: // * class <name-class>(<parameters>) // * def <name-function>(<parameters>) // * ... // * def <name-function>(<parameters>) // // the equivalent xhtml code is: // <ul> // <li> // <div>class <name-class>(<parameters>)</div> // <ul> // <li> // <div>def <name-function>(<parameters>)</div> // </li> // ... // <li> // <div>def <name-function>(<parameters>)</div> // </li> // </ul> // </li> // </ul> $renderer->listu_open(); // <ul> $renderer->listitem_open(1); // <li> $renderer->listcontent_open(); // <div> if ($tree[0][2] == 1) { $renderer->strong_open(); // start bold $renderer->cdata($tree[0][0]); $renderer->strong_close(); // stop bold } else { $renderer->cdata($tree[0][0]); } if ($docstr == 1) { $renderer->doc .= "<blockquote class='quote_pycode'>"; $renderer->emphasis_open(); foreach ($all_brief[0] as $line) { $renderer->doc .= $line . "<br />"; } $renderer->emphasis_close(); $renderer->doc .= "</blockquote>"; } $renderer->listcontent_close(); // </div> $renderer->listu_open(); // <ul> start indented foreach ($tree as $key => $val) { if ($key == 0) { continue; } $renderer->listitem_open(2); // <li> $renderer->listcontent_open(); // <div> if ($val[2] == 1) { $renderer->strong_open(); // start bold } elseif ($val[2] == 2) { $renderer->deleted_open(); // start strike out } $renderer->internallink($namespace . ":" . $val[1], $val[0]); if ($val[2] == 1) { $renderer->strong_close(); // stop bold } elseif ($val[2] == 2) { $renderer->deleted_close(); // stop strike out } if ($docstr == 1) { $renderer->doc .= "<blockquote class='quote_pycode'>"; $renderer->emphasis_open(); foreach ($all_brief[$key] as $line) { $renderer->doc .= $line . "<br />"; } $renderer->emphasis_close(); $renderer->doc .= "</blockquote>"; } $renderer->listcontent_close(); // </div> $renderer->listitem_close(); // </li> } $renderer->listu_close(); // </ul> stop indented $renderer->listitem_close(); // </li> $renderer->listu_close(); // </ul> } /** * Print a notificaton which informs that the copy of <file> * is out-of-date. * * Only users with a level permission greater than or equal to 2, * can flush it away (see DokuWiki's ACL): * level | 0 1 2 4 8 16 255 * permission| none read edit create upload delete admin * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) * @param (str) $loc_url the url to the raw code of the copy of <file> in * <srv-path>/lib/plugins/pycode/tmp/<host>/<repo>/<branch>/<file> * @param (arr) $code_new the updated code got from the repository * @param (int) $sl the number of the starting line of the code * @param (int) $el the number of the ending line of the code */ private function _print_note(Doku_Renderer $renderer, $loc_url, $code_new, $sl, $el) { global $INFO; $perm = $INFO["perm"]; $note = $this->getLang("note"); $code_new = base64_encode(serialize($code_new)); $renderer->doc .= "<div class='notify'>"; $renderer->cdata($note); if ($perm >= AUTH_EDIT) { $renderer->doc .= "<form method='post'>"; $renderer->doc .= "<input type='hidden' name='url' value=$loc_url>"; $renderer->doc .= "<input type='hidden' name='new' value=$code_new>"; $renderer->doc .= "<input type='hidden' name='start' value=$sl>"; $renderer->doc .= "<input type='hidden' name='end' value=$el>"; $renderer->doc .= "<input class='button ok_pycode' type='submit' name='submit' value='Ok'>"; $renderer->doc .= "</form>"; } $renderer->doc .= "</div>"; } /** * Print an error message which informs that the code between the * specified lines is out of range. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) */ private function _print_notfound_lns(Doku_Renderer $renderer) { $notfound_lns = $this->getLang("notfound-lns"); $renderer->doc .= "<div class='error'>"; $renderer->cdata($notfound_lns); $renderer->doc .= "</div>"; } /** * Print an error message which informs that the given function was * not found in <file>. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) */ private function _print_notfound_def(Doku_Renderer $renderer) { $notfound_def = $this->getLang("notfound-def"); $renderer->doc .= "<div class='error'>"; $renderer->cdata($notfound_def); $renderer->doc .= "</div>"; } /** * Print an error message which informs that the given class was * not found in <file>. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) */ private function _print_notfound_cls(Doku_Renderer $renderer) { $notfound_cls = $this->getLang("notfound-cls"); $renderer->doc .= "<div class='error'>"; $renderer->cdata($notfound_cls); $renderer->doc .= "</div>"; } /** * Prints an error message which informs that the flag is not correct. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) */ private function _print_wrong_flag(Doku_Renderer $renderer) { $wrong_flag = $this->getLang("wrong-flag"); $renderer->doc .= "<div class='error'>"; $renderer->cdata($wrong_flag); $renderer->doc .= "</div>"; } /** * Print an error message which informs that the file is not * a Python script. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) */ private function _print_wrong_lang(Doku_Renderer $renderer) { $wrong_lang = $this->getLang("wrong-lang"); $renderer->doc .= "<div class='error'>"; $renderer->cdata($wrong_lang); $renderer->doc .= "</div>"; } /** * Print an error message which informs that there was some problems * to contact the repository. * * @param (obj) $renderer object reference to the Doku_Render class, * defined in /inc/parser/renderer.php, which defines functions for the * output (see also /lib/styles/screen.css for some styles) */ private function _print_error(Doku_Renderer $renderer) { $error = $this->getLang("error"); $renderer->doc .= "<div class='error'>"; $renderer->cdata($error); $renderer->doc .= "</div>"; } }