0, the function is applied at most $count times.
/// If count is < 0, the function is applied at most $this->limit times.
/// In both cases, the function stops if the $text isn't changing anymore (i.e. callback returns the same text).
/// The callback receives the $text as a parameter, if repeat is called with more params, they are also sent to the callback.
function repeat($func, $count, $text) {
$args = func_get_args();
$args = array_slice($args, 2);
$func = $this->_fwrap($func);
if($count < 0)
$count = $this->limit;
if($count > 0) do {
$args[0] = $text;
$text = call_user_func_array($func, $args);
} while(--$count && strcmp($args[0], $text));
return $text;
}
/// str_repace wrapper. Replace string $from with $to in text $text.
function ssub($text, $from, $to) {
return str_replace($from, $to, $text);
}
/// Replace $regexp with $replacement in $text.
/// $count is the same as in repeat().
function gsub($text, $regexp, $replacement, $count = 1) {
return ($count == 1) ?
preg_replace($regexp, $replacement, $text) :
$this->repeat('.gsub', $count, $text, $regexp, $replacement);
}
/// Replace $regexp in $text using $func as a callback function.
/// Callback must follow preg_replace_callback rules (i.e. it takes an array of matches).
/// Function names starting with a dot are treated as methods of $this object,
/// i.e. gfun(foo, bar, '.baz') is the same as gfun(foo, bar, array($this, 'baz'))
function gfun($text, $regexp, $func, $count = 1) {
$func = $this->_fwrap($func);
return ($count == 1) ?
preg_replace_callback($regexp, $func, $text) :
$this->repeat('.gfun', $count, $text, $regexp, $func);
}
function _fwrap($func) {
if(!is_array($func) && $func[0] == '.')
return array(&$this, substr($func, 1));
return $func;
}
/// Replace the keys of $pairs with the values of $pairs.
/// $pairs is an associative array, whose keys are regexps and values are replacements.
function ksub($text, $pairs, $count = 1) {
return ($count == 1) ?
preg_replace(array_keys($pairs), array_values($pairs), $text) :
$this->repeat('.gsub', $count, $text, array_keys($pairs), array_values($pairs));
}
/// Replace the keys of $pairs by calling corresponding callbacks.
/// $pairs is an associative array, whose keys are regexps and values are callback functions.
function kfun($text, $pairs, $count = 1) {
foreach($pairs as $regexp => $func)
$pairs[$regexp] = $this->_fwrap($func);
return ($count == 1) ?
$this->_kfun($text, $pairs) :
$this->repeat('._kfun', $count, $text, $pairs);
}
function _kfun($text, $pairs) {
foreach($pairs as $regexp => $func)
$text = preg_replace_callback($regexp, $func, $text);
return $text;
}
/// preg_match wrapper. Returns an array of matches or an empty array.
function match($text, $regexp) {
if(!preg_match($regexp, $text, $m))
return array();
return $m;
}
/// preg_match_all wrapper. Returns an array of matches or an empty array.
function match_all($text, $regexp, $flags = 0) {
if(!preg_match_all($regexp, $text, $m, $flags))
return array();
return $m;
}
}
/// "Virtual clipboard" functions.
class PregClipboard extends PregParser
{
/// Array of strings that are currently in clipboard.
/// Can be freely changed.
var $data = array();
var $_uid;
/// uid is an unique id of this clipboard.
/// It's recommended to use characters in range \\x01-\\x07
function PregClipboard($uid = 0) {
static $_c = 0;
$this->_uid = preg_quote($uid ? $uid : chr(++$_c));
}
/// Find strings in $text that match $regexp, replace them with 'invisible' markers
/// (consisting of the characters in range \\x01-\\x19) and copy them to the clipboard.
/// If $group != 0, copies $group-th captured regexp subgroup (by default, the whole matched string).
function cut($text, $regexp, $group = 0) {
$this->_catgrp = $group;
return $this->gfun($text, $regexp, '._cut');
}
function _cut($m) {
return $this->copy($m[$this->_catgrp]);
}
/// Place the string in the clipboard and return a marker for it.
function copy($str) {
$this->data[] = $str;
$n = strval(count($this->data) - 1);
return $this->_uid . "\x08" .
strtr($n, '0123456789', "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19") . "\x08";
}
/// Insert strings from the clipboard back into the text.
function paste($text) {
return $this->gfun($text, "~($this->_uid)\x08([\\x10-\\x19]+)\\x08~", '._paste', -1);
}
function _paste($m) {
$n = intval(strtr($m[2], "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19", '0123456789'));
return isset($this->data[$n]) ? $this->data[$n] : '';
}
/// Clear the clipboard.
function clear() {
$this->data = array();
}
}
/// Macro parser.
class MakrellParser extends PregParser
{
/// list of built-in types (assoc array type_name => regexp)
var $types = array(
'int' => '\d+',
'string' => '(?:\"(?:\\\\.|[^\"])*\")|(?:\'(?:\\\\.|[^\'])*\')',
'id' => '[a-zA-Z_]\w*',
'alpha' => '[a-zA-Z]+',
'alnum' => '[a-zA-Z0-9]+',
'ns' => '\S+',
'any' => '[\s\S]+?',
'nobr' => '[^\n]+',
'br' => '[\n]+',
);
var $_basedir = '';
var $_macros = array();
/// Process the $text according to makrell rules and return parsed text.
function parse($text) {
$text = $this->repeat('._parse', -1, $text);
if(isset($this->_quotes)) {
$text = $this->_quotes->paste($text);
unset($this->_quotes);
}
return $text;
}
/// Parse the text in a file and return parsed text.
function parse_file($path) {
$this->_basedir = dirname(realpath($path));
return $this->parse(file_get_contents($path));
}
function _parse($text) {
$text = $this->repeat('._extract_macros', -1, $text);
$text = $this->repeat('._expand_macros', -1, $text);
return $text;
}
function _extract_macros($text) {
// find longest {{{
if(!$m = $this->match_all($text, '~\{{2,}~'))
return $text;
$len = max(array_map('strlen', $m[0]));
while($len > 1) {
// replace {{{ }}} with \x1a \x1b to make regexp simpler
$cx = array("\x1a", "\x1b");
$cs = array(str_repeat('{', $len), str_repeat('}', $len));
$t = $this->ssub($text, $cs, $cx);
// replace expressions that have $len braces
$t = $this->gfun($t, "~(?:^|\n) ([^\n\x1a]*) \x1a ([^\x1a\x1b]*) \x1b~xi", '._extract_one');
// put dangling braces back
$t = $this->ssub($t, $cx, $cs);
// if expressions were found and parsed - return right now
// this function will be restarted by the caller
if(strcmp($t, $text))
return $t;
// no, $len braces don't form any valid expression, try with less braces
$len--;
}
return $text;
}
function _extract_one($m) {
list(, $key, $body) = $m;
if(strlen(trim($key))) {
// a macro, like "foo {{ bar }}"
$this->macro($key, $body);
return '';
}
// a command like "{{ include blah }}"
if(!$m = $this->match($body, "~^\s*(\w+)(|\s.*)$~"))
return ''; // no command word -- comment. remove it.
$cmd = "command_{$m[1]}";
if(!method_exists($this, $cmd))
return ''; // no such command -- remove
return $this->$cmd($m[2]);
}
function _expand_macros($text) {
foreach($this->_macros as $key => $body) {
$this->_macrobody = $body;
$text = $this->gfun($text, $key, '._expand_one');
}
return $text;
}
function _expand_one($m) {
$body = $this->_macrobody;
foreach($m as $var => $value)
$body = $this->ssub($body, "\x1a{$var}\x1a", $value);
return $body;
}
/// Define the new macro with the key $key and body $body.
function macro($key, $body) {
if(!strlen($key = trim($key)))
return;
$this->_macrovars = array();
if($key[0] != '/') {
// if pattern is not a regexp
$key = preg_quote($key, '/');
// whitespace matches spaces+tabs
$key = $this->gsub($key, '~\s+~', '[ \\t]+');
// if pattern ends with an untyped var, it's assumed to be 'nobr'
if($this->match($key, '~@[a-z]+$~'))
$key .= '\:nobr';
// a variable is @foo or @foo:bar or @:bar
$key = $this->gfun($key, '~@([a-z]*\\\\:[0-9a-z]+|[a-z]+)~', '._parse_var_in_key');
$key = "/$key/";
}
// reduce trailing whitespace in body
$body = $this->gsub($body, '~^\s+|\s+$~', ' ');
// parse vars in body
$this->_macros[$key] = $this->_parse_vars_in_body($body);
}
function _parse_var_in_key($m) {
$s = explode('\\:', $m[1]);
$this->_macrovars[] = $s[0];
$re = isset($s[1]) && isset($this->types[$s[1]]) ? $this->types[$s[1]] : '.+?';
return "($re)";
}
function _parse_vars_in_body($body) {
$body = $this->gsub($body, '~@(\d\d?)~', "\x1a$1\x1a");
foreach($this->_macrovars as $n => $var) {
if(strlen($var))
$body = $this->ssub($body, "@$var", "\x1a" . ($n + 1) . "\x1a");
}
return $body;
}
/// Built-in 'type' command.
function command_type($body) {
// {{ type email .+?@.+ }}
if($m = $this->match(trim($body), "~^(\w+)(.*)$~"))
$this->types[$m[1]] = trim($m[2]);
return '';
}
/// Built-in 'include' command.
function command_include($body) {
// {{ include foobar }}
$path = trim($body, " \t\r\n\'\"");
if(strlen($this->_basedir) && !$this->match($path, '~^([a-z]:)?[/\\\\]~i'))
$path = $this->_basedir . '/' . $path;
return file_get_contents($path);
}
/// Built-in 'quote' command.
function command_quote($body) {
// {{ quote dont expand macros here }}
if(!isset($this->_quotes))
$this->_quotes = new PregClipboard("\x1c");
return $this->_quotes->copy($body);
}
function _dbg($text) {
$text = preg_replace('~[\x00-\x08\x0b-\x1f\x7F-\xFF]~e', "sprintf('\\x%02x', ord('$0'))", $text);
$q = "";
foreach(explode("\n", $text) as $n => $s)
$q .= sprintf("%04d: %s\n", $n + 1, $s);
return $q;
}
}
/// Main class, template engine.
class Makrell extends MakrellParser
{
/// If not empty, parsed templates will be cached in this directory
var $cachedir = '';
var $_vars = array();
/// When called with two arguments, sets a template variable $var to $value.
/// When called with one argument (which should be a hash of variables) adds all variables to the template.
function set($var, $value = null) {
if(is_array($var))
$this->_vars = array_merge($this->_vars, $var);
else
$this->_vars[$var] = $value;
}
/// Parse and include the template file and return the evaluated text.
function render($path) {
if(strlen($this->cachedir))
return $this->_render_file($this->_parse_cache($path));
else
return $this->_render_text($this->parse_file($path));
}
function _parse_cache($path) {
$path = realpath($path);
$cachedir = rtrim(strtr($this->cachedir, '\\', '/'), '/');
if(strlen($cachedir)) $cachedir .= '/';
$outpath = $cachedir . 'mak_' . md5($path);
if(!is_readable($outpath) || filemtime($path) >= filemtime($outpath)) {
$text = $this->parse_file($path);
$fp = fopen($outpath, "wb");
fwrite($fp, $text);
fclose($fp);
}
return $outpath;
}
function _render_file($__path) {
extract($this->_vars);
ob_start();
include($__path);
return ob_get_clean();
}
function _render_text($___text) {
extract($this->_vars);
ob_start();
eval("?>$___text");
return ob_get_clean();
}
}
?>