<?php
/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2021 The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
class CallbackGenerator
{
/**
* @var array Path to callbacks in keys, callback signature in values
*/
public $callbacks = [
'tags.*.attributes.*.filterChain.*' => [
'attrValue' => '*',
'attrName' => 'string'
],
'tags.*.filterChain.*' => [
'tag' => '!Tag',
'tagConfig' => '!Object'
]
];
/**
* @var Encoder
*/
protected $encoder;
/**
* Constructor
*/
public function __construct()
{
$this->encoder = new Encoder;
}
/**
* Replace all callbacks in given config
*
* @param array $config Original config
* @return array Modified config
*/
public function replaceCallbacks(array $config)
{
foreach ($this->callbacks as $path => $params)
{
$config = $this->mapArray($config, explode('.', $path), $params);
}
return $config;
}
/**
* Build the list of arguments used in a callback invocation
*
* @param array $params Callback parameters
* @param array $localVars Known vars from the calling scope
* @return string JavaScript code
*/
protected function buildCallbackArguments(array $params, array $localVars)
{
// Remove 'parser' as a parameter, since there's no such thing in JavaScript
unset($params['parser']);
// Rebuild the local vars map to include global vars and computed values
$available = array_combine(array_keys($localVars), array_keys($localVars));
$available += [
'innerText' => '(tag.getEndTag() ? text.substr(tag.getPos() + tag.getLen(), tag.getEndTag().getPos() - tag.getPos() - tag.getLen()) : "")',
'logger' => 'logger',
'openTags' => 'openTags',
'outerText' => 'text.substr(tag.getPos(), (tag.getEndTag() ? tag.getEndTag().getPos() + tag.getEndTag().getLen() - tag.getPos() : tag.getLen()))',
'registeredVars' => 'registeredVars',
'tagText' => 'text.substr(tag.getPos(), tag.getLen())',
'text' => 'text'
];
$args = [];
foreach ($params as $k => $v)
{
if (isset($v))
{
// Param by value
$args[] = $this->encoder->encode($v);
}
elseif (isset($available[$k]))
{
// Param by name that matches a local expression
$args[] = $available[$k];
}
else
{
$args[] = 'registeredVars[' . json_encode($k) . ']';
}
}
return implode(',', $args);
}
/**
* Generate a function from a callback config
*
* @param array $config Callback config
* @param array $params Param names as keys, param types as values
* @return Code
*/
protected function generateFunction(array $config, array $params)
{
$js = (string) $config['js'];
// returnFalse() and returnTrue() can be used as-is
if ($js === 'returnFalse' || $js === 'returnTrue')
{
return new Code($js);
}
// Add an empty list of params if none is set
$config += ['params' => []];
$src = $this->getHeader($params);
$src .= 'function(' . implode(',', array_keys($params)) . '){';
$src .= 'return ' . $this->parenthesizeCallback($js);
$src .= '(' . $this->buildCallbackArguments($config['params'], $params) . ');}';
return new Code($src);
}
/**
* Generate a function header for given signature
*
* @param array $params Param names as keys, param types as values
* @return string
*/
protected function getHeader(array $params)
{
// Prepare the function's header
$header = "/**\n";
foreach ($params as $paramName => $paramType)
{
$header .= '* @param {' . $paramType . '} ' . $paramName . "\n";
}
$header .= "* @return {*}\n";
$header .= "*/\n";
return $header;
}
/**
* Replace callbacks in given config array
*
* @param array $array Original config
* @param string[] $path Path to callbacks
* @param array $params Default params
* @return array Modified config
*/
protected function mapArray(array $array, array $path, array $params)
{
$key = array_shift($path);
$keys = ($key === '*') ? array_keys($array) : [$key];
foreach ($keys as $key)
{
if (!isset($array[$key]))
{
continue;
}
$array[$key] = (empty($path)) ? $this->generateFunction($array[$key], $params) : $this->mapArray($array[$key], $path, $params);
}
return $array;
}
/**
* Add parentheses to a function literal, if necessary
*
* Will return single vars as-is, and will put anything else between parentheses
*
* @param string $callback Original callback
* @return string Modified callback
*/
protected function parenthesizeCallback($callback)
{
return (preg_match('(^[.\\w]+$)D', $callback)) ? $callback : '(' . $callback . ')';
}
}