Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.00% covered (success)
94.00%
188 / 200
53.85% covered (warning)
53.85%
7 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateLayoutBlockCompiler
93.97% covered (success)
93.97%
187 / 199
53.85% covered (warning)
53.85%
7 / 13
46.46
0.00% covered (danger)
0.00%
0 / 1
 compileTextInline
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
5
 compileTextBlock
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 compileTextLabeled
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 compileCardBox
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 compileFieldLabel
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 compileFieldValue
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 compileFieldPair
84.00% covered (warning)
84.00%
21 / 25
0.00% covered (danger)
0.00%
0 / 1
7.20
 compileKeyValueGrid
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
5.17
 compileRichTextBox
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 compileCtaButton
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 compileDividerSpacer
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 compileConditionalWrapper
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 resolveValueExpr
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
7.05
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Template\Application\Service\BlockCompiler;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Template\Domain\Model\TemplateBlock;
12
13/**
14 * Layout, typography, and conditional block compiler.
15 *
16 * Compiles structural layout primitives, fields, labels, text blocks,
17 * and conditional logic containers into HTML/Twig markup.
18 *
19 * @package App\Core\Template\Application\Service\BlockCompiler
20 */
21final class TemplateLayoutBlockCompiler
22{
23    public const DEFAULT_RECORD_ID_OR_PREFIX = '{{ record.prefix_number|default(record.id) }}';
24
25    /**
26     * Compiles inline single-line text primitive.
27     *
28     * @param TemplateBlock $block Block instance.
29     * @return string Compiled HTML.
30     */
31    public function compileTextInline(TemplateBlock $block): string
32    {
33        $props = $block->props;
34        $source = (string)($props['source'] ?? 'field');
35        $rawTag = (string)($props['tag'] ?? 'div');
36        $tag = in_array($rawTag, ['div', 'span', 'h1', 'h2', 'h3', 'h4'], true)
37            ? $rawTag
38            : 'div';
39        $align = htmlspecialchars((string)($props['align'] ?? 'left'), ENT_QUOTES, 'UTF-8');
40        $weight = htmlspecialchars((string)($props['font_weight'] ?? 'normal'), ENT_QUOTES, 'UTF-8');
41
42        $valExpr = match ($source) {
43            'static'   => htmlspecialchars((string)($props['content'] ?? ''), ENT_QUOTES, 'UTF-8'),
44            'template' => (string)($props['content'] ?? ''),
45            default    => $this->resolveValueExpr(
46                (string)($props['field_key'] ?? 'name'),
47                (string)($props['format'] ?? 'text')
48            ),
49        };
50
51        return sprintf(
52            '<%s class="template-text-inline" style="text-align:%s;font-weight:%s;margin:4px 0;">%s</%s>',
53            $tag,
54            $align,
55            $weight,
56            $valExpr,
57            $tag
58        );
59    }
60
61    /**
62     * Compiles multi-line text paragraph block primitive.
63     *
64     * @param TemplateBlock $block Block instance.
65     * @return string Compiled HTML.
66     */
67    public function compileTextBlock(TemplateBlock $block): string
68    {
69        $props = $block->props;
70        $source = (string)($props['source'] ?? 'template');
71        $align = htmlspecialchars((string)($props['align'] ?? 'left'), ENT_QUOTES, 'UTF-8');
72        $fontSize = htmlspecialchars((string)($props['font_size'] ?? '9.5pt'), ENT_QUOTES, 'UTF-8');
73
74        $content = match ($source) {
75            'field'  => $this->resolveValueExpr(
76                (string)($props['field_key'] ?? 'description'),
77                (string)($props['format'] ?? 'text')
78            ),
79            'static' => nl2br(htmlspecialchars((string)($props['content'] ?? ''), ENT_QUOTES, 'UTF-8')),
80            default  => (string)($props['content'] ?? '{{ record.description|default(\'\')|nl2br }}'),
81        };
82
83        return sprintf(
84            '<div class="template-text-block" style="text-align:%s;font-size:%s;line-height:1.5;margin:8px 0;">'
85            . '%s</div>',
86            $align,
87            $fontSize,
88            $content
89        );
90    }
91
92    /**
93     * Compiles labeled text primitive (Label + Value pair).
94     *
95     * @param TemplateBlock $block Block instance.
96     * @return string Compiled HTML.
97     */
98    public function compileTextLabeled(TemplateBlock $block): string
99    {
100        $props = $block->props;
101        $label = htmlspecialchars((string)($props['label'] ?? 'Etykieta'), ENT_QUOTES, 'UTF-8');
102        $fieldKey = (string)($props['field_key'] ?? 'name');
103        $format = (string)($props['format'] ?? 'text');
104        $layout = (string)($props['layout'] ?? 'stacked');
105        $valExpr = $this->resolveValueExpr($fieldKey, $format);
106
107        if ($layout === 'inline') {
108            return sprintf(
109                '<div class="template-text-labeled" style="display:flex;align-items:center;gap:8px;margin:4px 0;'
110                . 'padding:4px 0;border-bottom:1px solid #e2e8f0;">'
111                . '<span style="font-size:8.5pt;color:#64748b;">%s:</span>'
112                . '<strong style="font-size:9.5pt;color:#0f172a;">%s</strong></div>',
113                $label,
114                $valExpr
115            );
116        }
117
118        return sprintf(
119            '<div class="template-text-labeled" style="margin:4px 0;padding:6px;background:#f8fafc;'
120            . 'border:1px solid #e2e8f0;border-radius:4px;">'
121            . '<div style="font-size:8pt;text-transform:uppercase;color:#64748b;font-weight:600;">%s</div>'
122            . '<div style="font-size:10pt;font-weight:bold;color:#0f172a;">%s</div></div>',
123            $label,
124            $valExpr
125        );
126    }
127
128    /**
129     * Compiles titled container card box primitive.
130     *
131     * @param TemplateBlock $block Block instance.
132     * @return string Compiled HTML.
133     */
134    public function compileCardBox(TemplateBlock $block): string
135    {
136        $props = $block->props;
137        $title = htmlspecialchars((string)($props['title'] ?? 'Karta Informacyjna'), ENT_QUOTES, 'UTF-8');
138        $content = (string)($props['content'] ?? '');
139
140        return sprintf(
141            '<div class="template-card-box" style="margin:8px 0;border:1px solid #cbd5e1;border-radius:6px;'
142            . 'background:#ffffff;overflow:hidden;">'
143            . '<div style="padding:6px 12px;background:#f1f5f9;border-bottom:1px solid #cbd5e1;'
144            . 'font-size:9pt;font-weight:bold;color:#1e293b;text-transform:uppercase;">%s</div>'
145            . '<div style="padding:10px 12px;font-size:9pt;color:#334155;">%s</div></div>',
146            $title,
147            $content !== '' ? $content : '{{ record.details|default(\'\') }}'
148        );
149    }
150
151    /**
152     * Compiles Field Label block.
153     *
154     * @param TemplateBlock $block Block instance.
155     * @return string Compiled HTML.
156     */
157    public function compileFieldLabel(TemplateBlock $block): string
158    {
159        $label = (string)($block->props['label'] ?? $block->props['field_key'] ?? 'Pole');
160        $fontWeight = (string)($block->props['font_weight'] ?? 'semibold');
161        $textCase = (string)($block->props['text_case'] ?? 'none');
162        $color = (string)($block->props['color'] ?? '#64748b');
163        $fontSize = (string)($block->props['font_size'] ?? '9pt');
164
165        $style = sprintf(
166            'font-size:%s;color:%s;font-weight:%s;text-transform:%s;display:block;margin-bottom:2px;',
167            htmlspecialchars($fontSize, ENT_QUOTES, 'UTF-8'),
168            htmlspecialchars($color, ENT_QUOTES, 'UTF-8'),
169            htmlspecialchars($fontWeight, ENT_QUOTES, 'UTF-8'),
170            htmlspecialchars($textCase, ENT_QUOTES, 'UTF-8')
171        );
172
173        return '<span class="template-field-label" style="' . $style . '">'
174            . '{{ field_label(' . var_export($label, true) . ') }}'
175            . '</span>';
176    }
177
178    /**
179     * Compiles Field Value block.
180     *
181     * @param TemplateBlock $block Block instance.
182     * @return string Compiled HTML.
183     */
184    public function compileFieldValue(TemplateBlock $block): string
185    {
186        $field = trim((string)($block->props['field_key'] ?? 'name'));
187        $format = (string)($block->props['format'] ?? 'text');
188        $currency = (string)($block->props['currency'] ?? 'PLN');
189        $default = (string)($block->props['default'] ?? '-');
190        $fontSize = (string)($block->props['font_size'] ?? '10pt');
191        $fontWeight = (string)($block->props['font_weight'] ?? 'bold');
192        $color = (string)($block->props['color'] ?? '#1e293b');
193        $align = (string)($block->props['align'] ?? 'left');
194        $prefix = (string)($block->props['prefix'] ?? '');
195        $suffix = (string)($block->props['suffix'] ?? '');
196
197        $style = sprintf(
198            'font-size:%s;color:%s;font-weight:%s;text-align:%s;display:block;',
199            htmlspecialchars($fontSize, ENT_QUOTES, 'UTF-8'),
200            htmlspecialchars($color, ENT_QUOTES, 'UTF-8'),
201            htmlspecialchars($fontWeight, ENT_QUOTES, 'UTF-8'),
202            htmlspecialchars($align, ENT_QUOTES, 'UTF-8')
203        );
204
205        $twigExpr = match ($format) {
206            'currency' => sprintf('record.%s|format_currency(%s)', $field, var_export($currency, true)),
207            'date'     => sprintf('record.%s|format_date', $field),
208            'datetime' => sprintf('record.%s|format_datetime', $field),
209            'badge'    => sprintf('status_badge(record.%s)', $field),
210            default    => sprintf('record.%s|default(%s)', $field, var_export($default, true)),
211        };
212
213        return '<div class="template-field-value" style="' . $style . '">'
214            . htmlspecialchars($prefix, ENT_QUOTES, 'UTF-8')
215            . '{{ ' . $twigExpr . ' }}'
216            . htmlspecialchars($suffix, ENT_QUOTES, 'UTF-8')
217            . '</div>';
218    }
219
220    /**
221     * Compiles Field Pair (Label + Value combined) block.
222     *
223     * @param TemplateBlock $block Block instance.
224     * @return string Compiled HTML.
225     */
226    public function compileFieldPair(TemplateBlock $block): string
227    {
228        $label = (string)($block->props['label'] ?? $block->props['field_key'] ?? 'Etykieta');
229        $field = trim((string)($block->props['field_key'] ?? 'name'));
230        $layout = (string)($block->props['layout'] ?? 'stacked');
231        $format = (string)($block->props['format'] ?? 'text');
232        $default = (string)($block->props['default'] ?? '-');
233
234        $valTwig = match ($format) {
235            'currency' => sprintf('record.%s|format_currency', $field),
236            'date'     => sprintf('record.%s|format_date', $field),
237            'datetime' => sprintf('record.%s|format_datetime', $field),
238            'badge'    => sprintf('status_badge(record.%s)', $field),
239            default    => sprintf('record.%s|default(%s)', $field, var_export($default, true)),
240        };
241
242        if ($layout === 'inline') {
243            return '<div class="template-field-pair is-inline" style="margin:4px 0;display:flex;'
244                . 'align-items:center;justify-content:space-between;border-bottom:1px dashed #e2e8f0;padding:4px 0;">'
245                . '<span style="font-size:9pt;color:#64748b;">{{ field_label('
246                . var_export($label, true) . ') }}:</span>'
247                . '<strong style="font-size:10pt;color:#1e293b;">{{ ' . $valTwig . ' }}</strong>'
248                . '</div>';
249        }
250
251        return '<div class="template-field-pair is-stacked" style="margin:6px 0;padding:6px 10px;'
252            . 'background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;">'
253            . '<span style="font-size:8pt;text-transform:uppercase;color:#64748b;display:block;margin-bottom:2px;">'
254            . '{{ field_label(' . var_export($label, true) . ') }}</span>'
255            . '<strong style="font-size:10pt;color:#1e293b;">{{ ' . $valTwig . ' }}</strong>'
256            . '</div>';
257    }
258
259    /**
260     * Compiles Key-Value Grid block.
261     *
262     * @param TemplateBlock $block Block instance.
263     * @return string Compiled HTML.
264     */
265    public function compileKeyValueGrid(TemplateBlock $block): string
266    {
267        $items = isset($block->props['items']) && is_array($block->props['items'])
268            ? $block->props['items']
269            : [
270                ['label' => 'Numer dokumentu', 'value' => self::DEFAULT_RECORD_ID_OR_PREFIX],
271                ['label' => 'Data wystawienia', 'value' => '{{ context.today|format_date }}'],
272            ];
273
274        $html = '<table style="width:100%;border-collapse:collapse;margin:12px 0;"><tr>';
275        $colCount = 0;
276        foreach ($items as $item) {
277            $label = htmlspecialchars((string)($item['label'] ?? ''), ENT_QUOTES, 'UTF-8');
278            $val = (string)($item['value'] ?? '');
279            $html .= '<td style="width:50%;padding:6px 12px;border:1px solid #e2e8f0;background:#f8fafc;">'
280                . '<span style="font-size:8pt;text-transform:uppercase;color:#64748b;'
281                . 'display:block;">' . $label . '</span>'
282                . '<strong style="font-size:10pt;color:#1e293b;">' . $val . '</strong>'
283                . '</td>';
284            $colCount++;
285            if ($colCount % 2 === 0) {
286                $html .= '</tr><tr>';
287            }
288        }
289        $html .= '</tr></table>';
290
291        return $html;
292    }
293
294    /**
295     * Compiles Rich Text Box block.
296     *
297     * @param TemplateBlock $block Block instance.
298     * @return string Compiled HTML.
299     */
300    public function compileRichTextBox(TemplateBlock $block): string
301    {
302        $content = (string)($block->props['content'] ?? '<p>Paragraph content...</p>');
303        return '<div class="template-rich-text" style="margin:10px 0;line-height:1.6;">' . $content . '</div>';
304    }
305
306    /**
307     * Compiles Call-to-Action Button block.
308     *
309     * @param TemplateBlock $block Block instance.
310     * @return string Compiled HTML.
311     */
312    public function compileCtaButton(TemplateBlock $block): string
313    {
314        $url = (string)($block->props['url'] ?? '{{ system.app_url }}');
315        $label = htmlspecialchars((string)($block->props['label'] ?? 'View details'), ENT_QUOTES, 'UTF-8');
316
317        return '<div style="text-align:center;margin:24px 0;">'
318            . '<a href="' . $url . '" style="background:#2563eb;color:#ffffff;text-decoration:none;'
319            . 'padding:12px 28px;border-radius:6px;font-weight:bold;display:inline-block;">'
320            . $label . '</a></div>';
321    }
322
323    /**
324     * Compiles horizontal divider or vertical spacer primitive.
325     *
326     * @param TemplateBlock $block Block instance.
327     * @return string Compiled HTML.
328     */
329    public function compileDividerSpacer(TemplateBlock $block): string
330    {
331        $props = $block->props;
332        $kind = (string)($props['divider_type'] ?? 'line');
333        if ($kind === 'spacer') {
334            $height = htmlspecialchars((string)($props['height'] ?? '20px'), ENT_QUOTES, 'UTF-8');
335            return sprintf('<div class="template-spacer" style="height:%s;width:100%%;"></div>', $height);
336        }
337
338        return '<hr class="template-divider" style="border:0;border-top:1px solid #cbd5e1;margin:12px 0;" />';
339    }
340
341    /**
342     * Compiles Conditional Wrapper block ({% if ... %} children {% endif %}).
343     *
344     * @param TemplateBlock $block Block instance.
345     * @param callable(TemplateBlock): string $nodeCompiler Callback to compile child nodes.
346     * @return string Compiled HTML with Twig conditional logic.
347     */
348    public function compileConditionalWrapper(TemplateBlock $block, callable $nodeCompiler): string
349    {
350        $condition = trim((string)($block->props['condition'] ?? 'true'));
351        if ($condition === '') {
352            $condition = 'true';
353        }
354
355        $childHtml = '';
356        foreach ($block->children as $child) {
357            $childHtml .= $nodeCompiler($child) . "\n";
358        }
359
360        return sprintf("{%% if %s %%}\n%s{%% endif %%}", $condition, $childHtml);
361    }
362
363    /**
364     * Resolves value expression based on field key and formatting option.
365     *
366     * @param string $fieldKey Data field key.
367     * @param string $format   Presentation format.
368     * @return string Twig expression.
369     */
370    public function resolveValueExpr(string $fieldKey, string $format): string
371    {
372        $safeKey = htmlspecialchars(trim($fieldKey), ENT_QUOTES, 'UTF-8');
373        if ($safeKey === '') {
374            $safeKey = 'id';
375        }
376
377        return match ($format) {
378            'currency' => sprintf('{{ record.%s|format_currency }}', $safeKey),
379            'date'     => sprintf('{{ record.%s|format_date }}', $safeKey),
380            'datetime' => sprintf('{{ record.%s|format_datetime }}', $safeKey),
381            'badge'    => sprintf('{{ status_badge(record.%s) }}', $safeKey),
382            default    => sprintf('{{ record.%s|default(\'\')|e }}', $safeKey),
383        };
384    }
385}