76 if(!$this->_prefix) $this->_prefix = \Rsi\Str::random();
77 foreach([
'{',
'[',
']',
'}'] as $char) $this->_unescape[$this->_escape[
'\\' . $char] = $this->_prefix . bin2hex($char)] = $char;
78 $this->
publish([
'langId'],self::READWRITE);
85 return [$this->langId =>
null];
88 protected function error($message,$file =
null,$line_no =
null,$context =
null){
89 $this->
component(
'log')->add($this->logPrio,$message,$file,$line_no,$context);
98 return require(str_replace(
'*',$lang_id,$this->_filename));
106 public function getStr($id,$default =
false){
107 $id = explode(
'.',$id);
108 $lang_id = $id[1] ?? $this->langId;
109 if(!array_key_exists($lang_id,$this->_strs)) $this->_strs[$lang_id] = array_change_key_case($this->
getStrs($lang_id));
110 return array_key_exists($id = strtolower($id[0]),$this->_strs[$lang_id]) ? $this->_strs[$lang_id][$id] : $default;
113 protected function macro($str,$type,&$tags,$callback){
115 (strpos($str,
'<' . ($tag = $this->
namespace .
':' . $type)) !==
false) &&
116 preg_match_all(
"/<$tag(?<attribs>(" . \
Rsi\Str::ATTRIBUTES_PATTERN .
")*)>(?<str>.*?)<\\/$tag>/s",$str,$matches,PREG_SET_ORDER)
117 )
foreach($matches as $match)
try{
118 $str = str_replace($match[0],call_user_func_array($callback,[$match[
'str'],\
Rsi\Str::attributes($match[
'attribs']),&$tags]),$str);
121 return $this->
error($e,[
'macro' => $type]);
128 $str = str_replace(array_keys($this->_escape),$this->_escape,$this->
macro($str,self::MACRO_ESCAPE,$tags,
function($str){
129 return str_replace($this->_unescape,array_keys($this->_unescape),$str);
131 if(strpos($str,
'<' . $this->
namespace .
':') !==
false){
132 $str = $this->
macro($str,self::MACRO_SECTION,$tags,
function($str,$attribs){
133 if(!array_key_exists($id = $attribs[
'id'] ?? $this->_prefix . $this->_sectionIndex,$this->_section)) $this->_section[$id] = $str;
134 $tag = $this->
namespace . ':' . self::MACRO_SECTION;
135 return "<$tag id='$id'>" . $this->_sectionIndex++ . "</$tag>";
137 $str = $this->
macro($str,self::MACRO_CACHE,$tags,function($str,$attribs,&$tags){
138 if(!($key = $attribs['key'] ?? null)) $key = md5($str);
139 elseif(preg_match_all('/\\[([\\w\\-]+)\\]/',$key,$matches,PREG_SET_ORDER)) foreach($matches as list($full,$tag)){
140 if(is_array($value = $this->value($tag,null,$tags))) $value = md5(serialize($value));
141 $key = str_replace($full,$value === null ? '' : $value,$key);
143 $key .= '-' . ($attribs['lang'] ?? $this->langId);
144 if(!is_numeric($ttl = $attribs['ttl'] ?? 0)) $ttl = $this->
evaluate($ttl,null,$tags);
145 if(!array_key_exists($key,$this->_cache)) $this->_cache[$key] = $ttl ? $this->
component('cache')->fetch($this->cacheKeyPrefix . $key) : false;
146 $tag = $this->namespace . ':' . self::MACRO_CACHE;
147 return "<$tag key='$key' ttl='$ttl'>" . ($this->_cache[$key] === false ? $str : '') . "</$tag>";
149 $str = $this->
macro($str,self::MACRO_CONST,$tags,function($name){
150 return constant($name);
152 $str = $this->
macro($str,self::MACRO_UNITS,$tags,function($tag,$attribs,&$tags){
154 foreach(\Rsi\Number::units($value = $tags[$tag] ?? 0,\Rsi\Record::explode($attribs['units'] ?? null,',','='),$value * ($attribs['tolerance'] ?? 0) / 100) as $unit => $count)
155 if($count || array_key_exists('empty',$attribs)) $result[] = compact('unit','count');
156 $tags[$attribs['tag'] ?? $tag . 'Units'] = $result;
159 $str = $this->
macro($str,self::MACRO_LOCATION,$tags,function($str){
160 return htmlspecialchars($this->component('location')->rewrite($str));
162 $str = $this->
macro($str,self::MACRO_URL,$tags,function($url,$attribs){
163 return $this->
component('cache')->fetch($this->urlKeyPrefix . $url,$attribs['ttl'] ?? $this->urlTtl,function() use ($url){
164 $this->
component(
'log')->debug(
"Loading external content from '$url'",__FILE__,__LINE__);
165 return preg_match(
'/^https?:\\/\\/(?!localhost|127\\.0\\.0\\.|\\[[0:]+1\\])/i',$url) ? file_get_contents($url) : $url;
168 $str = $this->
macro($str,self::MACRO_RIGHT,$tags,
function($str,$attribs){
169 return ($this->
component(
'user')->authorized($attribs[
'right'] ??
null,$attribs[
'level'] ??
null) xor ($attribs[
'not'] ??
false)) ? $str :
null;
177 if($str && (strpos($str,
'<' . $this->
namespace .
':') !==
false)){
178 $str = $this->
macro($str,self::MACRO_SECTION,$tags,
function($str,$attribs,$tags){
179 $str = $this->_section[$id = $attribs[
'id']];
180 $this->_section[$id] =
null;
181 return $this->
trans($str,$tags);
183 $str = $this->
macro($str,self::MACRO_FRAGMENT,$tags,
function($str,$attribs){
184 $fragments = $this->session->fragments ?: [];
185 $fragments[$id = $attribs[
'id'] ??
'fragment-' . \Rsi\Str::random()] = $str;
186 $this->session->fragments = $fragments;
187 return ($delay = $attribs[
'delay'] ??
null)
188 ?
"<div class='fred-fragment' id='$id'><script>document.addEventListener('DOMContentLoaded',function(){" .
189 "setTimeout(function(){ fred.controller.fragment('#$id','$id') },$delay * 1000)" .
190 "},true)</script></div>"
193 $str = $this->
macro($str,self::MACRO_CACHE,$tags,
function($str,$attribs){
194 $key = $attribs[
'key'];
195 if(($this->_cache[$key] ??
false) !==
false)
return $this->_cache[$key];
196 if($ttl = $attribs[
'ttl'] ??
false) $this->
component(
'cache')->save($this->cacheKeyPrefix . $key,$str,$ttl);
197 return $this->_cache[$key] = $str;
199 $str = $this->
macro($str,self::MACRO_ROUTE,$tags,
function($controller_name,$params){
200 $controller_name = explode(
'.',$controller_name);
201 if(array_key_exists(
'_',$params)){
202 foreach(($params[
'_'] ==
'*' ? array_keys($_GET) : explode(
',',$params[
'_'])) as $key)
203 if(!array_key_exists($key,$params) && ($value = $this->
component(
'request')->complex($key))) $params[$key] = $value;
206 return htmlspecialchars($this->
component(
'router')->reverse($controller_name[0],$controller_name[1] ??
null,$params));
208 $str = $this->
macro($str,self::MACRO_SPACE,$tags,
function($str){
209 return trim(preg_replace(
'/\\s*\\n\\s*/',
"\n",preg_replace(
'/>\\s+</',
'><',$str)));
211 $str = $this->
macro($str,self::MACRO_REPLACE,$tags,
function($str,$attribs){
212 if(!array_key_exists(
'from',$attribs))
return $str;
213 if(substr($from = $attribs[
'from'],0,1) !=
'/') $from =
'/' . preg_quote($from,
'/') .
'/';
214 return preg_replace($from,$attribs[
'to'] ??
null,$str);
216 $str = $this->
macro($str,self::MACRO_IMAGE,$tags,
function($url,$attribs){
217 return $this->
component(
'cache')->fetch($this->imageKeyPrefix . $url .
'?' . http_build_query($attribs),$attribs[
'ttl'] ?? $this->imageTtl,
function() use ($url,$attribs){
218 unset($attribs[
'ttl']);
219 foreach([
'width',
'height',
'background'] as $key)
if(array_key_exists($key,$attribs) && ($attribs[$key] ==
'auto')) $attribs[$key] =
true;
220 return $this->
component(
'html')->img($url,$attribs);
233 if($tags)
foreach($tags as $tag => $value){
234 switch(substr($tag,0,1)){
235 case '!': $tag = substr($tag,1);
237 default:
if(!is_object($value)) $value = is_array($value) ? $this->
escape($value) : ($value ? htmlspecialchars($value) : $value);
239 $escaped[$tag] = $value;
244 protected function value($tag,$params,&$tags){
245 if(\
Rsi\Str::numeric($tag))
return $tag;
248 foreach(($keys = explode(
'.',$tag)) as $key){
249 if(is_object($value))
try{
250 $value = $this->
escape([$value->$key])[0];
253 $this->
error(
"Property '$key' does not exists for tag '$tag'",__FILE__,__LINE__,compact(
'value'));
257 elseif(!is_array($value) || !array_key_exists($key,$value)){
261 else $value = $value[$key];
264 return is_object($value) ? strval($value) : $value;
267 $this->
error(
"Could not convert object for tag '$tag' to string",__FILE__,__LINE__,compact(
'value'));
269 foreach($tags as $key => $value)
if((substr($key,0,1) ==
'@') && !strcasecmp($keys[0],trim($key,
'@!'))){
270 $value = [$keys[0] => call_user_func($value,$tag,$tags,$params)];
271 $tags += substr($key,1,1) ==
'!' ? $value : $this->
escape($value);
272 return \Rsi\Record::iget($tags,$keys);
274 \Rsi\Record::set($tags,$keys,
null);
279 $expr = strtr($expr,[$this->_prefix .
true =>
'',
'?' => is_array($tag) ? implode(
',',$tag) : $tag]);
280 foreach($tags as $key => $value)
if(preg_match($mask =
'/\\b' . preg_quote($tag = trim($key,
'@!'),
'/') .
'\\b/',$expr)){
281 if(is_array($value = $this->
value($tag,
null,$tags))) $value = implode(
',',$value);
282 elseif(preg_match(
'/^\\d{4}-\\d{2}-\\d{2}/',$value)) $value =
"'$value'";
283 elseif(!is_numeric($value)) $value = $value ? 1 : 0;
284 $expr = preg_replace($mask,$value,$expr);
286 return \Rsi\Number::evaluate($expr);
290 return $length ===
null ? substr($value,$start) : substr($value,$start,$length);
294 return str_replace($search,$replace,$value);
298 return preg_replace($pattern,$replace,$value);
301 protected function formatHash($value,$algo,$prefix =
null,$suffix =
null){
302 return hash($algo,$prefix . $value . $suffix);
312 protected function format($tag,$value,$params,&$tags){
313 foreach(explode(
'|',$params) as $format)
if($format)
try{
314 if(substr($format,0,1) ==
'=') $value = $this->
evaluate(substr($format,1),$tag,$tags);
315 elseif(preg_match(
'/(\w+)\\((.*)\\)$/',$format,$match)){
316 $func_name =
'format' . ucfirst($match[1]);
317 $params = array_merge([$value],strlen($match[2]) ? array_map(
'urldecode',explode(
',',$match[2])) : []);
318 if(method_exists($component = $this,$func_name) || method_exists($component = $this->
component(
'local'),$func_name))
319 $value = call_user_func_array([$component,$func_name],$params);
320 else $value = call_user_func_array(
322 array_merge([self::EVENT_FORMAT . $match[1],$this],$params)
326 $value = \Rsi\Str::transform($value,$format,$count);
327 if(!$count) $value = date($format,is_numeric($value) ? $value : strtotime($value));
331 $value = $this->
error($e,compact(
'tag',
'value',
'params'));
334 return is_array($value) ? false : $value;
350 protected function replace($str,$full,$index,$type,$tag,$operator,$params,$value,&$tags,&$strs){
351 $default = $replace = $raw =
false;
352 if($type)
switch($type){
354 $tags[$tag] = substr($params,1);
360 if(!in_array($tag,$strs)) $strs[] = $tag;
361 $replace = \Rsi\Str::transform($this->
preProcess($this->
getStr($tag,$default),$tags),$params = explode(
'|',$params));
362 $raw = !in_array(self::MACRO_ESCAPE,$params);
365 if(substr($params,0,2) ==
'|=') $value = $this->
evaluate(substr($params,2),$tag,$tags);
366 if($value) $replace = $this->_prefix .
true;
370 if(substr($params,0,2) ==
'|=') $value = $this->
evaluate(substr($params,2),$tag,$tags);
371 if(!$value) $replace = $this->_prefix .
true;
375 $replace = count((array)$value);
379 if(is_array($value)){
380 $params = explode(
'|',substr($params,1),2);
381 $replace = $this->
format($tag,array_sum(\
Rsi\Record::column($value,$params[0])),$params[1] ??
null,$tags) . $this->_prefix .
true;
385 if($value && (is_array($value) || (is_numeric($value) && ($value > 0) && ($value = range(1,$value))))){
386 $replace = $this->_prefix .
true;
387 $sub = str_replace(array_keys($this->_unescape),$this->_unescape,substr($params,1));
390 foreach($value as $key => $record) $replace .=
391 $this->
trans($sub,(is_array($record) ? $record : []) + [
'v' => $record,
'k' => $key,
'i' => $i++,
'r' => --$r] + $tags);
395 $options = explode(
'|',substr($params,1));
396 $random = $value ==
'?' ? rand(0,count($options) - 1) :
false;
397 foreach($options as $key => $option){
398 $i = strpos($option,
':');
399 if($i ===
false) $replace = $option;
400 elseif($random ===
false ? \
Rsi\Str::operator($value,$operator ?:
'==',substr($option,0,$i)) : $key == $random){
401 $replace = substr($option,$i + 1);
405 if(!\Rsi::nothing($replace)) $replace = $this->
trans(str_replace(array_keys($this->_unescape),$this->_unescape,$replace),$tags) . $this->_prefix .
true;
408 $replace = json_encode($value);
412 $params = explode(
'|',substr($params,1));
414 $html->escape =
false;
415 $replace = call_user_func_array([$html,array_shift($params)],array_merge([$value],$params));
418 $replace = $this->
error($e,compact(
'tag',
'params'));
422 $replace = $this->
format($tag,$value,$params,$tags);
426 if(\
Rsi\Str::operator(is_array($value) ? count($value) : str_replace($this->_unescape,array_keys($this->_unescape),$value ??
''),$operator,$params)) $replace = $this->_prefix .
true;
429 elseif(!\Rsi::nothing($value))
430 $replace = $this->
format($tag,$value,$params,$tags) . $this->_prefix .
true;
431 return substr($str,0,$index) . ($raw ? $replace : str_replace($this->_unescape,array_keys($this->_unescape),$replace ??
'')) . substr($str,$index + strlen($full));
439 while(($end = strpos($str,
'}')) && (($start = strrpos($str,
'{',$end - strlen($str))) !==
false)) $str =
440 substr($str,0,$start) .
441 (strpos($replace = substr($str,$start + 1,$end - $start - 1),$this->_prefix .
true) ===
false ?
'' : str_replace($this->_prefix .
true,
'',$replace)) .
442 substr($str,$end + 1);
454 protected function trans($str,$tags,&$steps =
null){
455 if(!$str)
return $str;
459 if($steps) $steps[
'after pre-processing'] = $str;
460 while($str && preg_match_all($this->regex,$str,$matches,PREG_SET_ORDER | PREG_OFFSET_CAPTURE)){
461 foreach($strs as $id){
462 if(!array_key_exists($id,$count)) $count[$id] = 0;
463 elseif(++$count[$id] >= $this->maxNested){
464 $this->
error(
"Sub-string '$id' nested more than {$this->maxNested} times",__FILE__,__LINE__,compact(
'str',
'tags'));
469 foreach(array_reverse($matches) as $match){
470 if(substr($type = $match[1][0],0,1) ==
'{') $type = $this->types[substr($type,1,-1)] ??
null;
472 $params = $match[4][0];
473 $value = in_array($type,[self::TYPE_TAG,self::TYPE_OPTIONAL,self::TYPE_SUBSTR]) ? null : $this->
value($tag,$params,$tags);
474 $str = $this->
replace($str,$match[0][0],$match[0][1],$type,$tag,$match[3][0],$params,$value,$tags,$strs);
476 if($steps) $steps[
'step ' . ++$step] = $str;
478 return str_replace($this->_prefix .
true,
'',str_replace(array_keys($this->_unescape),$this->_unescape,$this->
brush($str)));
486 public function str($str,$tags =
null){
487 if(!$str)
return $str;
488 $tags = $this->
escape(array_merge($this->_tags,$tags ?: []));
489 $steps = $this->_fred->debug ? [
'original string' => $orig = $str] :
null;
490 $str = $this->
trans($str,$tags,$steps);
491 if($steps) $steps[
'before post-processing'] = $str;
493 if($steps && count($steps) >= $this->minLogSteps) $this->
component(
'log')->debug(__CLASS__ .
"::trans('$orig',...)",__FILE__,__LINE__,[
494 'steps' => $steps + [
'result' => $str],
496 'prefix' => $this->_prefix,
497 'unescape' => $this->_unescape + [$this->_prefix .
true =>
'true']
508 public function id($id,$tags =
null,$default =
false){
509 return $this->
str($this->
getStr($id,$default),$tags);
513 $fragments = $this->session->fragments ?: [];
514 $fragment = $fragments[$id] ??
false;
515 unset($fragments[$id]);
516 $this->session->fragments = $fragments;
522 $this->_regex =
'/\\[(';
523 foreach($this->types as $type) $this->_regex .= preg_quote($type,
'/') .
'|';
526 '(\\w[\\w\\-\\/\\.]*\\w|\\w+)' .
527 '(==|!=|>=?|<=?|=?%|-?\\*-?|!\\*|[\\/!]\\/)?' .
535 $this->_tags = $value ===
false ? [] : array_merge($this->_tags,$value);
541 foreach($this->
constants(
'TYPE_') as $name => $value) $this->_types[strtolower($name)] = $value;
547 return $this->
id($key);
550 public function __call($func_name,$params){
551 return $this->
id($func_name,array_shift($params),$params ? array_shift($params) :
false);
555 return $this->
str($str,$tags);