Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.08% covered (success)
96.08%
49 / 51
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockPositions
96.08% covered (success)
96.08%
49 / 51
66.67% covered (warning)
66.67%
4 / 6
29
0.00% covered (danger)
0.00%
0 / 1
 grids
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 content
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
7.05
 rows
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 part
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 int
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 clamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Migration;
6
7use Cosray\Block\Layout;
8use Cosray\Block\Placement;
9use Cosray\Field\Blocks;
10use Cosray\Field\Definitions;
11use Cosray\Schema\Columns;
12
13/**
14 * Blocks content from before positions: each row list flowed, the
15 * browser's sparse row flow placing spans after an optional indent. The
16 * conversion places every top-level row where that flow put it, stores
17 * the grid the rows were laid out on next to them and drops the indent,
18 * from the rows and from the blocks of their splits. Spans are clamped
19 * the way the readers clamped them, so a row keeps the size it rendered
20 * with. A list that already holds a position is left placed as it is,
21 * which makes the conversion safe to run again.
22 */
23final class BlockPositions
24{
25    /**
26     * The grid of every Blocks field of a node class: `#[Columns]`, one
27     * column without it.
28     *
29     * @param class-string $class
30     * @return array<string, array{columns: int, min: int}>
31     */
32    public static function grids(string $class): array
33    {
34        $grids = [];
35
36        foreach (Definitions::for($class)->fields() as $name => $definition) {
37            if (!is_a($definition->type, Blocks::class, true)) {
38                continue;
39            }
40
41            $attribute = $definition->property->getAttributes(Columns::class)[0] ?? null;
42            $columns = $attribute?->newInstance();
43            $grids[$name] = [
44                'columns' => $columns->columns ?? 1,
45                'min' => $columns->min ?? 1,
46            ];
47        }
48
49        return $grids;
50    }
51
52    /**
53     * @param array<string, mixed> $content
54     * @param array<string, array{columns: int, min: int}> $grids
55     * @return array<string, mixed>
56     */
57    public static function content(array $content, array $grids): array
58    {
59        foreach ($grids as $name => $grid) {
60            $field = $content[$name] ?? null;
61
62            if (!is_array($field) || !is_array($field['value'] ?? null)) {
63                continue;
64            }
65
66            $columns = Blocks::storedColumns($field['columns'] ?? null, $grid['columns']);
67
68            foreach ($field['value'] as $locale => $rows) {
69                if (is_array($rows) && array_is_list($rows)) {
70                    $field['value'][$locale] = self::rows($rows, $columns, $grid['min']);
71                }
72            }
73
74            $content[$name] = [...$field, 'columns' => $columns];
75        }
76
77        return $content;
78    }
79
80    /**
81     * @param list<mixed> $rows
82     * @return list<mixed>
83     */
84    public static function rows(array $rows, int $columns, int $min): array
85    {
86        $min = min($min, $columns);
87        $placed = false;
88        $spans = [];
89
90        foreach ($rows as $row) {
91            $layout = is_array($row) && is_array($row['layout'] ?? null) ? $row['layout'] : [];
92            $placed = $placed || isset($layout['col'], $layout['row']);
93        }
94
95        foreach ($rows as $index => $row) {
96            if (!is_array($row)) {
97                continue;
98            }
99
100            $layout = is_array($row['layout'] ?? null) ? $row['layout'] : [];
101            $colspan = self::clamp(self::int($layout['colspan'] ?? null, $columns), $min, $columns);
102            $rowspan = self::clamp(self::int($layout['rowspan'] ?? null, 1), 1, Layout::MAX_ROWSPAN);
103            $indent = self::clamp(self::int($layout['indent'] ?? null, 0), 0, $columns - $colspan);
104            unset($layout['indent']);
105            $rows[$index]['layout'] = [...$layout, 'colspan' => $colspan, 'rowspan' => $rowspan];
106
107            if (is_array($row['blocks'] ?? null)) {
108                $rows[$index]['blocks'] = array_map(self::part(...), $row['blocks']);
109            }
110
111            if (!isset($layout['col'], $layout['row'])) {
112                $spans[$index] = ['colspan' => $colspan, 'rowspan' => $rowspan, 'indent' => $indent];
113            }
114        }
115
116        // A list with a position was converted, or written since: its rows
117        // without one are placed by the readers, below the others.
118        if ($placed) {
119            return array_values($rows);
120        }
121
122        $spots = Placement::flow(array_values($spans), $columns);
123
124        foreach (array_keys($spans) as $position => $index) {
125            $rows[$index]['layout'] = [...$rows[$index]['layout'], ...$spots[$position]];
126        }
127
128        return array_values($rows);
129    }
130
131    private static function part(mixed $part): mixed
132    {
133        if (is_array($part) && is_array($part['layout'] ?? null)) {
134            unset($part['layout']['indent']);
135        }
136
137        return $part;
138    }
139
140    private static function int(mixed $value, int $default): int
141    {
142        return is_int($value) || is_numeric($value) ? (int) $value : $default;
143    }
144
145    private static function clamp(int $value, int $min, int $max): int
146    {
147        return max($min, min($max, $value));
148    }
149}