Skip to content

Commit 20b16e7

Browse files
committed
refactor: harden structured layout mutation coverage
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent e17a2dd commit 20b16e7

10 files changed

Lines changed: 1102 additions & 477 deletions

src/Layout/LinearLayoutEngine.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
{
1616
private InlineStyleParser $styleParser;
1717
private LayoutStyleResolver $styleResolver;
18-
private StructuredLayoutRenderer $structuredLayoutRenderer;
18+
private StructuredLayoutRenderer $structuredRenderer;
1919

2020
public function __construct(?InlineStyleParser $styleParser = null)
2121
{
2222
$this->styleParser = $styleParser ?? new InlineStyleParser();
2323
$this->styleResolver = new LayoutStyleResolver();
24-
$this->structuredLayoutRenderer = new StructuredLayoutRenderer($this->styleParser, $this->styleResolver);
24+
$this->structuredRenderer = new StructuredLayoutRenderer($this->styleParser, $this->styleResolver);
2525
}
2626

2727
/**
@@ -30,7 +30,7 @@ public function __construct(?InlineStyleParser $styleParser = null)
3030
public function layout(array $nodes, float $width, float $height): LayoutResult
3131
{
3232
if ($this->requiresStructuredLayout($nodes)) {
33-
return $this->structuredLayoutRenderer->layout($nodes, $width, $height);
33+
return $this->structuredRenderer->layout($nodes, $width, $height);
3434
}
3535

3636
return $this->layoutLinear($nodes, $width, $height);
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Layout;
9+
10+
use LibreSign\XObjectTemplate\Css\StyleMap;
11+
use LibreSign\XObjectTemplate\Html\Node;
12+
13+
/** @internal */
14+
final readonly class StructuredBoxResolver
15+
{
16+
public function __construct(private LayoutStyleResolver $styleResolver)
17+
{
18+
}
19+
20+
/**
21+
* @param array{x: float, y: float, width: float, height: float} $availableBox
22+
* @return array{
23+
* margin: array{top: float, right: float, bottom: float, left: float},
24+
* box: array{x: float, y: float, width: float, height: float}
25+
* }
26+
*/
27+
public function resolveFlowPlacement(Node $node, StyleMap $style, array $availableBox): array
28+
{
29+
$margin = $this->styleResolver->parseBoxSpacingRelative(
30+
$this->styleResolver->styleValue($style, 'margin', '0'),
31+
$availableBox['width'],
32+
$availableBox['height'],
33+
);
34+
35+
return [
36+
'margin' => $margin,
37+
'box' => [
38+
'x' => $availableBox['x'] + $margin['left'],
39+
'y' => $availableBox['y'] + $margin['top'],
40+
'width' => $this->resolveFlowWidth($node, $style, $availableBox, $margin),
41+
'height' => $this->resolveFlowHeight($node, $style, $availableBox),
42+
],
43+
];
44+
}
45+
46+
/**
47+
* @param array{x: float, y: float, width: float, height: float} $container
48+
* @return array{x: float, y: float, width: float, height: float}
49+
*/
50+
public function resolveAbsoluteBox(Node $node, StyleMap $style, array $container): array
51+
{
52+
$margin = $this->styleResolver->parseBoxSpacingRelative(
53+
$this->styleResolver->styleValue($style, 'margin', '0'),
54+
$container['width'],
55+
$container['height'],
56+
);
57+
58+
$resolvedWidth = $this->resolveAbsoluteWidth($node, $style, $container, $margin);
59+
$resolvedHeight = $this->resolveAbsoluteHeight($node, $style, $container, $margin);
60+
61+
return [
62+
'x' => $this->resolveAbsoluteX($style, $container, $resolvedWidth, $margin),
63+
'y' => $this->resolveAbsoluteY($style, $container, $resolvedHeight, $margin),
64+
'width' => $resolvedWidth,
65+
'height' => $resolvedHeight,
66+
];
67+
}
68+
69+
/**
70+
* @param array{x: float, y: float, width: float, height: float} $box
71+
* @return array{
72+
* padding: array{top: float, right: float, bottom: float, left: float},
73+
* contentBox: array{x: float, y: float, width: float, height: float}
74+
* }
75+
*/
76+
public function resolveContentBox(StyleMap $style, array $box): array
77+
{
78+
$padding = $this->styleResolver->parseBoxSpacingRelative(
79+
$this->styleResolver->styleValue($style, 'padding', '0'),
80+
$box['width'],
81+
$box['height'] > 0.0 ? $box['height'] : $box['width'],
82+
);
83+
84+
return [
85+
'padding' => $padding,
86+
'contentBox' => [
87+
'x' => $box['x'] + $padding['left'],
88+
'y' => $box['y'] + $padding['top'],
89+
'width' => max($box['width'] - $padding['left'] - $padding['right'], 0.0),
90+
'height' => max($box['height'] - $padding['top'] - $padding['bottom'], 0.0),
91+
],
92+
];
93+
}
94+
95+
/**
96+
* @param array{x: float, y: float, width: float, height: float} $contentBox
97+
* @return array{x: float, y: float, width: float, height: float}
98+
*/
99+
public function createChildContainer(array $contentBox, float $consumedHeight): array
100+
{
101+
return [
102+
'x' => $contentBox['x'],
103+
'y' => $contentBox['y'] + $consumedHeight,
104+
'width' => $contentBox['width'],
105+
'height' => $contentBox['height'] > 0.0
106+
? max($contentBox['height'] - $consumedHeight, 0.0)
107+
: 0.0,
108+
];
109+
}
110+
111+
/**
112+
* @param array{top: float, right: float, bottom: float, left: float} $padding
113+
*/
114+
public function resolveAutoContainerHeight(float $resolvedHeight, array $padding, float $contentHeight): float
115+
{
116+
$autoHeight = $padding['top'] + $contentHeight + $padding['bottom'];
117+
118+
return $resolvedHeight > 0.0 ? max($resolvedHeight, $autoHeight) : $autoHeight;
119+
}
120+
121+
/**
122+
* @param array{top: float, right: float, bottom: float, left: float} $margin
123+
* @param array{x: float, y: float, width: float, height: float} $availableBox
124+
*/
125+
private function resolveFlowWidth(Node $node, StyleMap $style, array $availableBox, array $margin): float
126+
{
127+
$resolvedWidth = $this->styleResolver->resolveRelativeDimension(
128+
$this->styleResolver->styleValue($style, 'width', ''),
129+
$availableBox['width'],
130+
);
131+
132+
if ($resolvedWidth > 0.0) {
133+
return $resolvedWidth;
134+
}
135+
136+
if ($node->tag === 'img') {
137+
return 32.0;
138+
}
139+
140+
return max($availableBox['width'] - $margin['left'] - $margin['right'], 0.0);
141+
}
142+
143+
/**
144+
* @param array{x: float, y: float, width: float, height: float} $availableBox
145+
*/
146+
private function resolveFlowHeight(Node $node, StyleMap $style, array $availableBox): float
147+
{
148+
$resolvedHeight = $this->styleResolver->resolveRelativeDimension(
149+
$this->styleResolver->styleValue($style, 'height', ''),
150+
$availableBox['height'],
151+
);
152+
153+
if ($resolvedHeight > 0.0) {
154+
return $resolvedHeight;
155+
}
156+
157+
return $node->tag === 'img' ? 32.0 : 0.0;
158+
}
159+
160+
/**
161+
* @param array{top: float, right: float, bottom: float, left: float} $margin
162+
* @param array{x: float, y: float, width: float, height: float} $container
163+
*/
164+
private function resolveAbsoluteWidth(Node $node, StyleMap $style, array $container, array $margin): float
165+
{
166+
$resolvedWidth = $this->styleResolver->resolveRelativeDimension(
167+
$this->styleResolver->styleValue($style, 'width', ''),
168+
$container['width'],
169+
);
170+
171+
if ($resolvedWidth > 0.0) {
172+
return $resolvedWidth;
173+
}
174+
175+
if ($node->tag === 'img') {
176+
return 32.0;
177+
}
178+
179+
return max($container['width'] - $margin['left'] - $margin['right'], 0.0);
180+
}
181+
182+
/**
183+
* @param array{top: float, right: float, bottom: float, left: float} $margin
184+
* @param array{x: float, y: float, width: float, height: float} $container
185+
*/
186+
private function resolveAbsoluteHeight(Node $node, StyleMap $style, array $container, array $margin): float
187+
{
188+
$resolvedHeight = $this->styleResolver->resolveRelativeDimension(
189+
$this->styleResolver->styleValue($style, 'height', ''),
190+
$container['height'],
191+
);
192+
193+
if ($resolvedHeight > 0.0) {
194+
return $resolvedHeight;
195+
}
196+
197+
if ($node->tag === 'img') {
198+
return 32.0;
199+
}
200+
201+
return max($container['height'] - $margin['top'] - $margin['bottom'], 0.0);
202+
}
203+
204+
/**
205+
* @param array{top: float, right: float, bottom: float, left: float} $margin
206+
* @param array{x: float, y: float, width: float, height: float} $container
207+
*/
208+
private function resolveAbsoluteX(StyleMap $style, array $container, float $resolvedWidth, array $margin): float
209+
{
210+
$left = $this->styleResolver->styleValue($style, 'left', '');
211+
if ($left !== '') {
212+
return $container['x']
213+
+ $this->styleResolver->resolveRelativeDimension($left, $container['width'])
214+
+ $margin['left'];
215+
}
216+
217+
$right = $this->styleResolver->styleValue($style, 'right', '');
218+
if ($right === '') {
219+
return $container['x'] + $margin['left'];
220+
}
221+
222+
return $container['x']
223+
+ max(
224+
$container['width']
225+
- $resolvedWidth
226+
- $this->styleResolver->resolveRelativeDimension($right, $container['width'])
227+
- $margin['right'],
228+
0.0,
229+
);
230+
}
231+
232+
/**
233+
* @param array{top: float, right: float, bottom: float, left: float} $margin
234+
* @param array{x: float, y: float, width: float, height: float} $container
235+
*/
236+
private function resolveAbsoluteY(StyleMap $style, array $container, float $resolvedHeight, array $margin): float
237+
{
238+
$top = $this->styleResolver->styleValue($style, 'top', '');
239+
if ($top !== '') {
240+
return $container['y']
241+
+ $this->styleResolver->resolveRelativeDimension($top, $container['height'])
242+
+ $margin['top'];
243+
}
244+
245+
$bottom = $this->styleResolver->styleValue($style, 'bottom', '');
246+
if ($bottom === '') {
247+
return $container['y'] + $margin['top'];
248+
}
249+
250+
return $container['y']
251+
+ max(
252+
$container['height']
253+
- $resolvedHeight
254+
- $this->styleResolver->resolveRelativeDimension($bottom, $container['height'])
255+
- $margin['bottom'],
256+
0.0,
257+
);
258+
}
259+
}

0 commit comments

Comments
 (0)