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