1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Js;
6:
7: use Atk4\Core\DiContainerTrait;
8: use Atk4\Data\Persistence;
9: use Atk4\Ui\Exception;
10: use Atk4\Ui\View;
11:
12: class JsExpression implements JsExpressionable
13: {
14: use DiContainerTrait;
15:
16: public string $template;
17:
18: /** @var array<int|string, mixed> */
19: public array $args;
20:
21: /**
22: * @param array<int|string, mixed> $args
23: */
24: public function __construct(string $template = '', array $args = [])
25: {
26: $this->template = $template;
27: $this->args = $args;
28: }
29:
30: #[\Override]
31: public function jsRender(): string
32: {
33: $namelessCount = 0;
34: $res = preg_replace_callback(
35: '~\[[\w]*\]|{[\w]*}~',
36: function ($matches) use (&$namelessCount): string {
37: $identifier = substr($matches[0], 1, -1);
38:
39: // allow template to contain []
40: if ($identifier === '') {
41: $identifier = $namelessCount++;
42: }
43:
44: if (!isset($this->args[$identifier])) {
45: throw (new Exception('Tag is not defined in template'))
46: ->addMoreInfo('tag', $identifier)
47: ->addMoreInfo('template', $this->template);
48: }
49:
50: $value = $this->args[$identifier];
51:
52: // no escaping for "{}"
53: if ($matches[0][0] === '{' && is_string($value)) {
54: return $value;
55: }
56:
57: $valueStr = $this->_jsEncode($value);
58: if ($value instanceof JsExpressionable && !str_ends_with($valueStr, ';')) {
59: $valueStr = '(' . $valueStr . ')';
60: }
61:
62: return $valueStr;
63: },
64: $this->template
65: );
66:
67: return trim($res);
68: }
69:
70: /**
71: * @param mixed $value
72: */
73: protected function _jsEncode($value): string
74: {
75: if ($value instanceof JsExpressionable) {
76: $res = $value->jsRender();
77: } elseif ($value instanceof View) {
78: $res = $this->_jsEncode('#' . $value->getHtmlId());
79: } elseif (is_array($value)) {
80: $array = [];
81: $assoc = !array_is_list($value);
82:
83: foreach ($value as $k => $v) {
84: $v = $this->_jsEncode($v);
85: $k = $this->_jsEncode($k);
86: if (!$assoc) {
87: $array[] = $v;
88: } else {
89: $array[] = $k . ': ' . $v;
90: }
91: }
92:
93: if ($assoc) {
94: $res = '{' . implode(', ', $array) . '}';
95: } else {
96: $res = '[' . implode(', ', $array) . ']';
97: }
98: } elseif (is_string($value)) {
99: $res = json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR);
100: $res = '\'' . str_replace('\'', '\\\'', str_replace('\\"', '"', substr($res, 1, -1))) . '\'';
101: } elseif (is_bool($value)) {
102: $res = $value ? 'true' : 'false';
103: } elseif (is_int($value)) {
104: // IMPORTANT: always convert large integers to string, otherwise numbers can be rounded by JS
105: $res = abs($value) < (2 ** 53) ? (string) $value : $this->_jsEncode((string) $value);
106: } elseif (is_float($value)) {
107: $res = Persistence\Sql\Expression::castFloatToString($value);
108: } elseif ($value === null) {
109: $res = 'null';
110: } else {
111: throw (new Exception('Argument is not renderable to JS'))
112: ->addMoreInfo('arg', $value);
113: }
114:
115: return $res;
116: }
117: }
118: