Test Failed
Push — develop ( 425fc0...4f2be8 )
by Paul
13:11
created

Builder::indexedId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 3
cts 4
cp 0.75
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.0625
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules\Html;
4
5
use GeminiLabs\SiteReviews\Arguments;
6
use GeminiLabs\SiteReviews\Contracts\BuilderContract;
7
use GeminiLabs\SiteReviews\Contracts\FieldContract;
8
use GeminiLabs\SiteReviews\Defaults\FieldDefaults;
9
use GeminiLabs\SiteReviews\Helper;
10
use GeminiLabs\SiteReviews\Helpers\Arr;
11
use GeminiLabs\SiteReviews\Helpers\Cast;
12
use GeminiLabs\SiteReviews\Helpers\Str;
13
14
/**
15
 * This class generates HTML tags without additional DOM markup.
16
 *
17
 * @method string a(string|array ...$params)
18
 * @method string button(string|array ...$params)
19
 * @method string div(string|array ...$params)
20
 * @method string form(string|array ...$params)
21
 * @method string h1(string|array ...$params)
22
 * @method string h2(string|array ...$params)
23
 * @method string h3(string|array ...$params)
24
 * @method string h4(string|array ...$params)
25
 * @method string h5(string|array ...$params)
26
 * @method string h6(string|array ...$params)
27
 * @method string i(string|array ...$params)
28
 * @method string img(string|array ...$params)
29
 * @method string input(string|array ...$params)
30
 * @method string label(string|array ...$params)
31
 * @method string li(string|array ...$params)
32
 * @method string nav(string|array ...$params)
33
 * @method string ol(string|array ...$params)
34
 * @method string optgroup(string|array ...$params)
35
 * @method string option(string|array ...$params)
36
 * @method string p(string|array ...$params)
37
 * @method string pre(string|array ...$params)
38
 * @method string section(string|array ...$params)
39
 * @method string select(string|array ...$params)
40
 * @method string small(string|array ...$params)
41
 * @method string span(string|array ...$params)
42
 * @method string textarea(string|array ...$params)
43
 * @method string ul(string|array ...$params)
44
 */
45
class Builder implements BuilderContract
46
{
47
    public const TAGS_FIELD = [
48
        'input', 'select', 'textarea',
49
    ];
50
51
    public const TAGS_STRUCTURE = [
52
        'div', 'form', 'nav', 'ol', 'section', 'ul',
53
    ];
54
55
    public const TAGS_TEXT = [
56
        'a', 'button', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'label', 'li', 'option', 'optgroup',
57
        'p', 'pre', 'small', 'span',
58
    ];
59
60
    public const TAGS_VOID = [
61
        'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source',
62
        'track', 'wbr',
63
    ];
64
65
    protected Arguments $args;
66
67
    protected string $tag;
68
69
    /**
70
     * This creates a new Builder instance to build an element.
71
     */
72 120
    public function __call(string $method, array $arguments): string
73
    {
74 120
        if (!is_array($args = array_shift($arguments))) {
75 20
            $text = Cast::toString($args);
76 20
            $args = Arr::consolidate(array_shift($arguments));
77 20
            $args['text'] = $text;
78
        }
79 120
        if (empty($method)) {
80
            return '';
81
        }
82 120
        $tag = Str::dashCase($method);
83 120
        return (new static())->build($tag, $args);
84
    }
85
86 130
    public function args(): Arguments
87
    {
88 130
        return $this->args ??= new Arguments();
89
    }
90
91
    /**
92
     * This uses the existing Builder instance to build an element
93
     * and overwrites existing tag and arguments
94
     * with the passed arguments.
95
     */
96 130
    public function build(string $tag, array $args = []): string
97
    {
98 130
        if (in_array($tag, static::TAGS_FIELD)) {
99 128
            $args = glsr(FieldDefaults::class)->merge($args);
100
        }
101 130
        $args = glsr()->filterArray("builder/{$tag}/args", $args, $this);
102 130
        $this->args = new Arguments($args);
103 130
        $this->tag = $tag;
104 130
        return $this->process();
105
    }
106
107
    /**
108
     * This uses the Field's builder instance/build method to build the element.
109
     */
110
    public function buildField(FieldContract $field): string
111
    {
112
        return $field->build();
113
    }
114
115
    public function field(array $args): FieldContract
116
    {
117
        return new Field($args);
118
    }
119
120
    public function set(string $key, $value): void
121
    {
122
        $this->args()->set($key, $value);
123
    }
124
125 130
    public function tag(): string
126
    {
127 130
        return $this->tag ??= '';
128
    }
129
130 113
    protected function buildElement(): string
131
    {
132 113
        $text = $this->args()->text;
133 113
        return $this->buildTagStart().$text.$this->buildTagEnd();
134
    }
135
136 128
    protected function buildFieldElement(): string
137
    {
138 128
        $method = Helper::buildMethodName('build', 'field', $this->tag(), 'element');
139 128
        return call_user_func([$this, $method]);
140
    }
141
142 34
    protected function buildFieldInputChoice(): string
143
    {
144 34
        $text = $this->args()->text ?: $this->args()->label;
145 34
        if (empty($text)) {
146 10
            return $this->buildVoidElement();
147
        }
148 25
        return $this->label([
149 25
            'for' => $this->args()->id,
150 25
            'text' => "{$this->buildVoidElement()} {$text}",
151 25
        ]);
152
    }
153
154 9
    protected function buildFieldInputChoices(): string
155
    {
156 9
        $index = 0;
157 9
        $values = array_keys($this->args()->options);
158 9
        return array_reduce($values, function ($carry, $value) use (&$index) {
159 9
            return $carry.$this->input([
160 9
                'checked' => in_array($value, $this->args()->cast('value', 'array')),
161 9
                'class' => $this->args()->class,
162 9
                'disabled' => $this->args()->disabled,
163 9
                'id' => $this->indexedId(++$index),
164 9
                'label' => $this->args()->options[$value],
165 9
                'name' => $this->args()->name,
166 9
                'required' => $this->args()->required,
167 9
                'tabindex' => $this->args()->tabindex,
168 9
                'type' => $this->args()->type,
169 9
                'value' => $value,
170 9
            ]);
171 9
        }, '');
172
    }
173
174 89
    protected function buildFieldInputElement(): string
175
    {
176 89
        if (!in_array($this->args()->type, ['checkbox', 'radio'])) {
177 55
            return $this->buildFieldLabel().$this->buildVoidElement();
178
        }
179 34
        if (empty($this->args()->options)) {
180 34
            return $this->buildFieldInputChoice();
181
        }
182 24
        return $this->buildFieldInputChoices();
183
    }
184
185 94
    protected function buildFieldLabel(): string
186
    {
187 94
        if ('hidden' === $this->args()->type) {
188 3
            return '';
189
        }
190 92
        if (empty($this->args()->label)) {
191 89
            return '';
192
        }
193 3
        return $this->label([
194 3
            'for' => $this->args()->id,
195 3
            'text' => $this->args()->label,
196 3
        ]);
197
    }
198
199 30
    protected function buildFieldSelectElement(): string
200
    {
201 30
        $options = $this->buildFieldSelectOptions();
202 30
        $select = $this->buildTagStart().$options.$this->buildTagEnd();
203 30
        return $this->buildFieldLabel().$select;
204
    }
205
206 2
    protected function buildFieldSelectOptgroup(array $options, string $label): string
207
    {
208 2
        $values = array_keys($options);
209 2
        $children = array_reduce($values, fn ($carry, $value) => $carry.$this->buildFieldSelectOption([
210 2
            'text' => $options[$value],
211 2
            'value' => $value,
212 2
        ]), '');
213 2
        return $this->optgroup([
214 2
            'label' => $label,
215 2
            'text' => $children,
216 2
        ]);
217
    }
218
219 30
    protected function buildFieldSelectOption(array $args): string
220
    {
221 30
        $selected = in_array($args['value'] ?? null, $this->args()->cast('value', 'array'));
222 30
        $args = wp_parse_args($args, [
223 30
            'selected' => $selected,
224 30
            'text' => '',
225 30
            'value' => '',
226 30
        ]);
227 30
        if (!is_array($args['text'])) {
228 30
            return $this->option($args);
229
        }
230
        // If $args['text'] is an array and has a title and text key then create an option tag
231
        // with a title attribute to provide accessibility when the text is made up of symbols.
232 2
        if (Arr::compare(array_keys($args['text']), ['text', 'title'])) {
233 1
            return $this->option(wp_parse_args($args['text'], $args));
234
        }
235 2
        return '';
236
    }
237
238 30
    protected function buildFieldSelectOptions(): string
239
    {
240 30
        $options = $this->args()->options;
241 30
        if ($this->args()->placeholder) {
242 4
            $options = Arr::prepend($options, $this->args()->placeholder, '');
243
        }
244 30
        $values = array_keys($options);
245 30
        return array_reduce($values, function ($carry, $value) use ($options) {
246 30
            $option = $this->buildFieldSelectOption([
247 30
                'text' => $options[$value],
248 30
                'value' => $value,
249 30
            ]);
250 30
            if (empty($option)) {
251 2
                return $carry.$this->buildFieldSelectOptgroup($options[$value], $value);
252
            }
253 29
            return $carry.$option;
254 30
        }, '');
255
    }
256
257 11
    protected function buildFieldTextareaElement(): string
258
    {
259 11
        $text = esc_html($this->args()->cast('value', 'string'));
260 11
        $textarea = $this->buildTagStart().$text.$this->buildTagEnd();
261 11
        return $this->buildFieldLabel().$textarea;
262
    }
263
264 115
    protected function buildTagEnd(): string
265
    {
266 115
        if (in_array($this->tag(), static::TAGS_VOID)) {
267
            return '';
268
        }
269 115
        return "</{$this->tag()}>";
270
    }
271
272 130
    protected function buildTagStart(): string
273
    {
274 130
        $attributes = glsr(Attributes::class)->{$this->tag()}($this->args()->toArray())->toString();
275 130
        $tag = trim("{$this->tag()} {$attributes}");
276 130
        if (in_array($this->tag(), static::TAGS_VOID)) {
277 90
            return "<{$tag} />";
278
        }
279 115
        return "<{$tag}>";
280
    }
281
282 90
    protected function buildVoidElement(): string
283
    {
284 90
        return $this->buildTagStart();
285
    }
286
287 24
    protected function indexedId(int $index): string
288
    {
289 24
        if (!empty($this->args()->id)) {
290 24
            return "{$this->args()->id}-{$index}";
291
        }
292
        return $this->args()->id;
293
    }
294
295 130
    protected function process(): string
296
    {
297 130
        glsr()->action('builder', $this); // This hook is used in PublicController to add styled classes
298 130
        if (in_array($this->tag(), static::TAGS_FIELD)) { // check this first
299 128
            $result = $this->buildFieldElement();
300 113
        } elseif (in_array($this->tag(), static::TAGS_VOID)) {
301 1
            $result = $this->buildVoidElement();
302
        } else {
303 113
            $result = $this->buildElement();
304
        }
305 130
        $result = glsr()->filterString('builder/result', $result, $this);
306 130
        return $result;
307
    }
308
}
309