1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View;
16:
17: use Cake\Core\Configure\Engine\PhpConfig;
18: use Cake\Core\InstanceConfigTrait;
19: use Cake\Utility\Hash;
20: use RuntimeException;
21:
22: /**
23: * Provides an interface for registering and inserting
24: * content into simple logic-less string templates.
25: *
26: * Used by several helpers to provide simple flexible templates
27: * for generating HTML and other content.
28: */
29: class StringTemplate
30: {
31: use InstanceConfigTrait {
32: getConfig as get;
33: }
34:
35: /**
36: * List of attributes that can be made compact.
37: *
38: * @var array
39: */
40: protected $_compactAttributes = [
41: 'allowfullscreen' => true,
42: 'async' => true,
43: 'autofocus' => true,
44: 'autoplay' => true,
45: 'checked' => true,
46: 'compact' => true,
47: 'controls' => true,
48: 'declare' => true,
49: 'default' => true,
50: 'defaultchecked' => true,
51: 'defaultmuted' => true,
52: 'defaultselected' => true,
53: 'defer' => true,
54: 'disabled' => true,
55: 'enabled' => true,
56: 'formnovalidate' => true,
57: 'hidden' => true,
58: 'indeterminate' => true,
59: 'inert' => true,
60: 'ismap' => true,
61: 'itemscope' => true,
62: 'loop' => true,
63: 'multiple' => true,
64: 'muted' => true,
65: 'nohref' => true,
66: 'noresize' => true,
67: 'noshade' => true,
68: 'novalidate' => true,
69: 'nowrap' => true,
70: 'open' => true,
71: 'pauseonexit' => true,
72: 'readonly' => true,
73: 'required' => true,
74: 'reversed' => true,
75: 'scoped' => true,
76: 'seamless' => true,
77: 'selected' => true,
78: 'sortable' => true,
79: 'truespeed' => true,
80: 'typemustmatch' => true,
81: 'visible' => true,
82: ];
83:
84: /**
85: * The default templates this instance holds.
86: *
87: * @var array
88: */
89: protected $_defaultConfig = [];
90:
91: /**
92: * A stack of template sets that have been stashed temporarily.
93: *
94: * @var array
95: */
96: protected $_configStack = [];
97:
98: /**
99: * Contains the list of compiled templates
100: *
101: * @var array
102: */
103: protected $_compiled = [];
104:
105: /**
106: * Constructor.
107: *
108: * @param array $config A set of templates to add.
109: */
110: public function __construct(array $config = [])
111: {
112: $this->add($config);
113: }
114:
115: /**
116: * Push the current templates into the template stack.
117: *
118: * @return void
119: */
120: public function push()
121: {
122: $this->_configStack[] = [
123: $this->_config,
124: $this->_compiled
125: ];
126: }
127:
128: /**
129: * Restore the most recently pushed set of templates.
130: *
131: * @return void
132: */
133: public function pop()
134: {
135: if (empty($this->_configStack)) {
136: return;
137: }
138: list($this->_config, $this->_compiled) = array_pop($this->_configStack);
139: }
140:
141: /**
142: * Registers a list of templates by name
143: *
144: * ### Example:
145: *
146: * ```
147: * $templater->add([
148: * 'link' => '<a href="{{url}}">{{title}}</a>'
149: * 'button' => '<button>{{text}}</button>'
150: * ]);
151: * ```
152: *
153: * @param string[] $templates An associative list of named templates.
154: * @return $this
155: */
156: public function add(array $templates)
157: {
158: $this->setConfig($templates);
159: $this->_compileTemplates(array_keys($templates));
160:
161: return $this;
162: }
163:
164: /**
165: * Compile templates into a more efficient printf() compatible format.
166: *
167: * @param string[] $templates The template names to compile. If empty all templates will be compiled.
168: * @return void
169: */
170: protected function _compileTemplates(array $templates = [])
171: {
172: if (empty($templates)) {
173: $templates = array_keys($this->_config);
174: }
175: foreach ($templates as $name) {
176: $template = $this->get($name);
177: if ($template === null) {
178: $this->_compiled[$name] = [null, null];
179: }
180:
181: $template = str_replace('%', '%%', $template);
182: preg_match_all('#\{\{([\w\._]+)\}\}#', $template, $matches);
183: $this->_compiled[$name] = [
184: str_replace($matches[0], '%s', $template),
185: $matches[1]
186: ];
187: }
188: }
189:
190: /**
191: * Load a config file containing templates.
192: *
193: * Template files should define a `$config` variable containing
194: * all the templates to load. Loaded templates will be merged with existing
195: * templates.
196: *
197: * @param string $file The file to load
198: * @return void
199: */
200: public function load($file)
201: {
202: $loader = new PhpConfig();
203: $templates = $loader->read($file);
204: $this->add($templates);
205: }
206:
207: /**
208: * Remove the named template.
209: *
210: * @param string $name The template to remove.
211: * @return void
212: */
213: public function remove($name)
214: {
215: $this->setConfig($name, null);
216: unset($this->_compiled[$name]);
217: }
218:
219: /**
220: * Format a template string with $data
221: *
222: * @param string $name The template name.
223: * @param array $data The data to insert.
224: * @return string|null Formatted string or null if template not found.
225: */
226: public function format($name, array $data)
227: {
228: if (!isset($this->_compiled[$name])) {
229: throw new RuntimeException("Cannot find template named '$name'.");
230: }
231: list($template, $placeholders) = $this->_compiled[$name];
232:
233: if (isset($data['templateVars'])) {
234: $data += $data['templateVars'];
235: unset($data['templateVars']);
236: }
237: $replace = [];
238: foreach ($placeholders as $placeholder) {
239: $replacement = isset($data[$placeholder]) ? $data[$placeholder] : null;
240: if (is_array($replacement)) {
241: $replacement = implode('', $replacement);
242: }
243: $replace[] = $replacement;
244: }
245:
246: return vsprintf($template, $replace);
247: }
248:
249: /**
250: * Returns a space-delimited string with items of the $options array. If a key
251: * of $options array happens to be one of those listed
252: * in `StringTemplate::$_compactAttributes` and its value is one of:
253: *
254: * - '1' (string)
255: * - 1 (integer)
256: * - true (boolean)
257: * - 'true' (string)
258: *
259: * Then the value will be reset to be identical with key's name.
260: * If the value is not one of these 4, the parameter is not output.
261: *
262: * 'escape' is a special option in that it controls the conversion of
263: * attributes to their HTML-entity encoded equivalents. Set to false to disable HTML-encoding.
264: *
265: * If value for any option key is set to `null` or `false`, that option will be excluded from output.
266: *
267: * This method uses the 'attribute' and 'compactAttribute' templates. Each of
268: * these templates uses the `name` and `value` variables. You can modify these
269: * templates to change how attributes are formatted.
270: *
271: * @param array|null $options Array of options.
272: * @param array|null $exclude Array of options to be excluded, the options here will not be part of the return.
273: * @return string Composed attributes.
274: */
275: public function formatAttributes($options, $exclude = null)
276: {
277: $insertBefore = ' ';
278: $options = (array)$options + ['escape' => true];
279:
280: if (!is_array($exclude)) {
281: $exclude = [];
282: }
283:
284: $exclude = ['escape' => true, 'idPrefix' => true, 'templateVars' => true] + array_flip($exclude);
285: $escape = $options['escape'];
286: $attributes = [];
287:
288: foreach ($options as $key => $value) {
289: if (!isset($exclude[$key]) && $value !== false && $value !== null) {
290: $attributes[] = $this->_formatAttribute($key, $value, $escape);
291: }
292: }
293: $out = trim(implode(' ', $attributes));
294:
295: return $out ? $insertBefore . $out : '';
296: }
297:
298: /**
299: * Formats an individual attribute, and returns the string value of the composed attribute.
300: * Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked'
301: *
302: * @param string $key The name of the attribute to create
303: * @param string|array $value The value of the attribute to create.
304: * @param bool $escape Define if the value must be escaped
305: * @return string The composed attribute.
306: */
307: protected function _formatAttribute($key, $value, $escape = true)
308: {
309: if (is_array($value)) {
310: $value = implode(' ', $value);
311: }
312: if (is_numeric($key)) {
313: return "$value=\"$value\"";
314: }
315: $truthy = [1, '1', true, 'true', $key];
316: $isMinimized = isset($this->_compactAttributes[$key]);
317: if (!preg_match('/\A(\w|[.-])+\z/', $key)) {
318: $key = h($key);
319: }
320: if ($isMinimized && in_array($value, $truthy, true)) {
321: return "$key=\"$key\"";
322: }
323: if ($isMinimized) {
324: return '';
325: }
326:
327: return $key . '="' . ($escape ? h($value) : $value) . '"';
328: }
329:
330: /**
331: * Adds a class and returns a unique list either in array or space separated
332: *
333: * @param array|string $input The array or string to add the class to
334: * @param array|string $newClass the new class or classes to add
335: * @param string $useIndex if you are inputting an array with an element other than default of 'class'.
336: * @return string|string[]
337: */
338: public function addClass($input, $newClass, $useIndex = 'class')
339: {
340: // NOOP
341: if (empty($newClass)) {
342: return $input;
343: }
344:
345: if (is_array($input)) {
346: $class = Hash::get($input, $useIndex, []);
347: } else {
348: $class = $input;
349: $input = [];
350: }
351:
352: // Convert and sanitise the inputs
353: if (!is_array($class)) {
354: if (is_string($class) && !empty($class)) {
355: $class = explode(' ', $class);
356: } else {
357: $class = [];
358: }
359: }
360:
361: if (is_string($newClass)) {
362: $newClass = explode(' ', $newClass);
363: }
364:
365: $class = array_unique(array_merge($class, $newClass));
366:
367: $input = Hash::insert($input, $useIndex, $class);
368:
369: return $input;
370: }
371: }
372: