Skip to content

Commit 3de58cd

Browse files
committed
[BUGFIX] Parse calc split over multiple lines
1 parent c29c15a commit 3de58cd

File tree

3 files changed

+212
-3
lines changed

3 files changed

+212
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Please also have a look at our
1919
### Fixed
2020

2121
- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384)
22+
- Fix parsing of `calc` expressions when a newline immediately precedes or
23+
follows a + or - operator (#1399)
2224

2325
### Documentation
2426

src/Value/CalcFunction.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
99
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
1010

11+
use function Safe\preg_match;
12+
1113
class CalcFunction extends CSSFunction
1214
{
1315
private const T_OPERAND = 1;
@@ -60,9 +62,8 @@ public static function parse(ParserState $parserState, bool $ignoreCase = false)
6062
if (\in_array($parserState->peek(), $operators, true)) {
6163
if (($parserState->comes('-') || $parserState->comes('+'))) {
6264
if (
63-
$parserState->peek(1, -1) !== ' '
64-
|| !($parserState->comes('- ')
65-
|| $parserState->comes('+ '))
65+
preg_match('/\\s/', $parserState->peek(1, -1)) !== 1
66+
|| preg_match('/\\s/', $parserState->peek(1, 1)) !== 1
6667
) {
6768
throw new UnexpectedTokenException(
6869
" {$parserState->peek()} ",
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sabberworm\CSS\Tests\Unit\Value;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sabberworm\CSS\OutputFormat;
9+
use Sabberworm\CSS\Parsing\ParserState;
10+
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
11+
use Sabberworm\CSS\Settings;
12+
use Sabberworm\CSS\Value\CalcFunction;
13+
use Sabberworm\CSS\Value\CalcRuleValueList;
14+
use Sabberworm\CSS\Value\Size;
15+
16+
/**
17+
* @covers \Sabberworm\CSS\Value\CalcFunction
18+
*/
19+
final class CalcFunctionTest extends TestCase
20+
{
21+
/**
22+
* @test
23+
*/
24+
public function parseSimpleCalc(): void
25+
{
26+
$css = 'calc(100% - 20px)';
27+
$calcFunction = $this->parse($css);
28+
29+
self::assertInstanceOf(CalcFunction::class, $calcFunction);
30+
self::assertSame('calc', $calcFunction->getName());
31+
32+
$args = $calcFunction->getArguments();
33+
self::assertCount(1, $args);
34+
self::assertInstanceOf(CalcRuleValueList::class, $args[0]);
35+
36+
/** @var CalcRuleValueList $value */
37+
$value = $args[0];
38+
$components = $value->getListComponents();
39+
self::assertCount(3, $components); // 100%, -, 20px
40+
41+
self::assertInstanceOf(Size::class, $components[0]);
42+
self::assertSame(100.0, $components[0]->getSize());
43+
self::assertSame('%', $components[0]->getUnit());
44+
45+
self::assertSame('-', $components[1]);
46+
47+
self::assertInstanceOf(Size::class, $components[2]);
48+
self::assertSame(20.0, $components[2]->getSize());
49+
self::assertSame('px', $components[2]->getUnit());
50+
}
51+
/**
52+
* @test
53+
*/
54+
public function parseNestedCalc(): void
55+
{
56+
$css = 'calc(100% - calc(20px + 1em))';
57+
$calcFunction = $this->parse($css);
58+
59+
/** @var CalcRuleValueList $value */
60+
$value = $calcFunction->getArguments()[0];
61+
$components = $value->getListComponents();
62+
63+
self::assertCount(3, $components);
64+
self::assertSame('-', $components[1]);
65+
66+
/** @var CalcFunction */
67+
$nestedCalc = $components[2];
68+
self::assertInstanceOf(CalcFunction::class, $nestedCalc);
69+
70+
/** @var CalcRuleValueList $nestedValue */
71+
$nestedValue = $nestedCalc->getArguments()[0];
72+
self::assertInstanceOf(CalcRuleValueList::class, $nestedValue);
73+
$nestedComponents = $nestedValue->getListComponents();
74+
75+
self::assertCount(3, $nestedComponents);
76+
self::assertSame('+', $nestedComponents[1]);
77+
}
78+
79+
/**
80+
* @test
81+
*/
82+
public function parseWithParentheses(): void
83+
{
84+
$css = 'calc((100% - 20px) * 2)';
85+
$calcFunction = $this->parse($css);
86+
87+
/** @var CalcRuleValueList $value */
88+
$value = $calcFunction->getArguments()[0];
89+
$components = $value->getListComponents();
90+
91+
self::assertCount(7, $components);
92+
self::assertSame('(', $components[0]);
93+
self::assertInstanceOf(Size::class, $components[1]); // 100%
94+
self::assertSame('-', $components[2]);
95+
self::assertInstanceOf(Size::class, $components[3]); // 20px
96+
self::assertSame(')', $components[4]);
97+
self::assertSame('*', $components[5]);
98+
self::assertInstanceOf(Size::class, $components[6]); // 2
99+
}
100+
101+
/**
102+
* @return array<string, array{0: string, 1: string}>
103+
*/
104+
public function provideValidOperatorSyntax(): array
105+
{
106+
return [
107+
'+ op' => ['calc(100% + 20px)', 'calc(100% + 20px)'],
108+
'- op' => ['calc(100% - 20px)', 'calc(100% - 20px)'],
109+
'* op' => ['calc(100% * 20)', 'calc(100% * 20)'],
110+
'* op no space' => ['calc(100%*20)', 'calc(100% * 20)'],
111+
'/ op' => ['calc(100% / 20)', 'calc(100% / 20)'],
112+
'/ op no space' => ['calc(100%/20)', 'calc(100% / 20)'],
113+
];
114+
}
115+
116+
/**
117+
* @test
118+
*
119+
* @dataProvider provideValidOperatorSyntax
120+
*/
121+
public function parseValidOperators(string $css, string $rendered): void
122+
{
123+
$calcFunction = $this->parse($css);
124+
$output = $calcFunction->render(OutputFormat::create());
125+
self::assertSame($rendered, $output);
126+
}
127+
128+
/**
129+
* @return array<string, array{0: string, 1: string}>
130+
*/
131+
public function provideMultiline(): array
132+
{
133+
return [
134+
'right newline' => ["calc(100% +\n20px)", 'calc(100% + 20px)'],
135+
'right and outer newline' => ["calc(\n100% +\n20px\n)", 'calc(100% + 20px)'],
136+
'left newline' => ["calc(100%\n+ 20px)", 'calc(100% + 20px)'],
137+
'both newline' => ["calc(100%\n+\n20px)", 'calc(100% + 20px)'],
138+
'tab whitespace' => ["calc(100%\t+\t20px)", 'calc(100% + 20px)'],
139+
'- op' => ["calc(100%\n-\n20px)", 'calc(100% - 20px)'],
140+
'/ op' => ["calc(100% /\n20)", 'calc(100% / 20)'],
141+
];
142+
}
143+
144+
/**
145+
* @test
146+
*
147+
* @dataProvider provideMultiline
148+
*/
149+
public function parseMultiline(string $css, string $rendered): void
150+
{
151+
$calcFunction = $this->parse($css);
152+
$output = $calcFunction->render(OutputFormat::create());
153+
self::assertSame($rendered, $output);
154+
}
155+
156+
/**
157+
* @return array<string, array{0: string}>
158+
*/
159+
public function provideInvalidSyntax(): array
160+
{
161+
return [
162+
'missing space around -' => ['calc(100%-20px)'],
163+
'missing space around +' => ['calc(100%+20px)'],
164+
'invalid operator' => ['calc(100% ^ 20px)'],
165+
];
166+
}
167+
168+
/**
169+
* @test
170+
*
171+
* @dataProvider provideInvalidSyntax
172+
*/
173+
public function parseThrowsExceptionForInvalidSyntax(string $css): void
174+
{
175+
$this->expectException(UnexpectedTokenException::class);
176+
$this->parse($css);
177+
}
178+
179+
/**
180+
* @test
181+
*/
182+
public function parseThrowsExceptionIfCalledWithWrongFunctionName(): void
183+
{
184+
$css = 'wrong(100% - 20px)';
185+
$parserState = new ParserState($css, Settings::create());
186+
187+
$this->expectException(UnexpectedTokenException::class);
188+
$this->expectExceptionMessage('calc');
189+
CalcFunction::parse($parserState);
190+
}
191+
192+
/**
193+
* Parse provided CSS as a CalcFunction
194+
*
195+
* @param string $css
196+
* @return CalcFunction
197+
*/
198+
private function parse(string $css): CalcFunction
199+
{
200+
$parserState = new ParserState($css, Settings::create());
201+
202+
$function = CalcFunction::parse($parserState);
203+
self::assertInstanceOf(CalcFunction::class, $function);
204+
return $function;
205+
}
206+
}

0 commit comments

Comments
 (0)