1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Chromabits\Nucleus\View;
13:
14: use Chromabits\Nucleus\Data\ArrayList;
15: use Chromabits\Nucleus\Data\ArrayMap;
16: use Chromabits\Nucleus\Exceptions\CoreException;
17: use Chromabits\Nucleus\Foundation\BaseObject;
18: use Chromabits\Nucleus\Foundation\Interfaces\ArrayableInterface;
19: use Chromabits\Nucleus\Meditation\Spec;
20: use Chromabits\Nucleus\Support\Html;
21: use Chromabits\Nucleus\View\Exceptions\InvalidAttributesException;
22: use Chromabits\Nucleus\View\Exceptions\NodeChildRenderingException;
23: use Chromabits\Nucleus\View\Exceptions\NodeRenderingException;
24: use Chromabits\Nucleus\View\Interfaces\RenderableInterface;
25: use Chromabits\Nucleus\View\Interfaces\SafeHtmlProducerInterface;
26:
27: 28: 29: 30: 31: 32: 33: 34:
35: class Node extends BaseObject implements
36: RenderableInterface,
37: SafeHtmlProducerInterface
38: {
39: 40: 41:
42: protected $tagName = null;
43:
44: 45: 46:
47: protected $attributes = [];
48:
49: 50: 51:
52: protected $selfClosing = false;
53:
54: 55: 56:
57: protected $content;
58:
59: 60: 61:
62: protected $spec;
63:
64: 65: 66: 67: 68: 69: 70: 71:
72: public function __construct(
73: $tagName,
74: $attributes,
75: $content,
76: $selfClosing = false
77: ) {
78: parent::__construct();
79:
80: $this->tagName = $tagName;
81: $this->attributes = array_merge(
82: $this->getDefaultAttributes(),
83: $attributes
84: );
85: $this->content = $content;
86: $this->selfClosing = $selfClosing;
87:
88: $this->spec = new Spec();
89: }
90:
91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104:
105: public function getDefaultAttributes()
106: {
107: return [];
108: }
109:
110: 111: 112: 113: 114: 115: 116: 117:
118: protected function renderAttribute($name, $value = null)
119: {
120: if ($value === null) {
121: return $name;
122: }
123:
124: if (is_array($value)) {
125: $value = implode(' ', $value);
126: }
127:
128: return vsprintf('%s="%s"', [
129: $name,
130: Html::escape((string) $value),
131: ]);
132: }
133:
134: 135: 136: 137: 138:
139: protected function renderAttributes()
140: {
141: return ArrayMap::of($this->attributes)
142: ->map(function ($value, $name) {
143: return $this->renderAttribute($name, $value);
144: })
145: ->join(' ');
146: }
147:
148: 149: 150: 151: 152: 153:
154: protected function renderContent()
155: {
156: if (is_string($this->content)
157: || $this->content instanceof SafeHtmlWrapper
158: || $this->content instanceof SafeHtmlProducerInterface
159: ) {
160: return Html::escape($this->content);
161: } elseif ($this->content instanceof RenderableInterface) {
162: return Html::escape($this->content->render());
163: } elseif (is_array($this->content)
164: || $this->content instanceof ArrayableInterface
165: ) {
166: return ArrayList::of($this->content)
167: ->map(function ($child) {
168: if (is_string($child)
169: || $child instanceof SafeHtmlWrapper
170: || $child instanceof SafeHtmlProducerInterface
171: ) {
172: return Html::escape($child);
173: } elseif ($child instanceof RenderableInterface) {
174: return Html::escape($child->render());
175: }
176:
177: throw new NodeChildRenderingException($child);
178: })
179: ->join();
180: }
181:
182: throw new NodeRenderingException($this->content);
183: }
184:
185: 186: 187: 188: 189: 190: 191:
192: public function render()
193: {
194: $result = $this->spec->check($this->attributes);
195: if ($result->failed()) {
196: throw new InvalidAttributesException($result);
197: }
198:
199: $attributes = $this->renderAttributes();
200: if (strlen($attributes)) {
201: $attributes = ' ' . $attributes;
202: }
203:
204: if ($this->selfClosing) {
205: return vsprintf(
206: '<%s%s/>',
207: [$this->tagName, $attributes]
208: );
209: }
210:
211: return vsprintf(
212: '<%s%s>%s</%s>',
213: [
214: $this->tagName,
215: $attributes,
216: $this->renderContent(),
217: $this->tagName,
218: ]
219: );
220: }
221:
222: 223: 224: 225: 226:
227: public function getSafeHtml()
228: {
229: return new SafeHtmlWrapper($this->render());
230: }
231: }
232: