Skip to content

Commit 21cdb91

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents c7acb76 + e828595 commit 21cdb91

File tree

3 files changed

+343
-24
lines changed

3 files changed

+343
-24
lines changed

src/HTMLAttributeBinder.php

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ public function bind(
3333
$element = $element->documentElement;
3434
}
3535

36-
$bindValue = $this->normalizeBindValue($value);
36+
$bindValue = $this->standardiseBindValue($value);
3737
$attributesToRemove = [];
38+
$attributeUpdates = [];
3839
foreach($element->attributes as $attributeName => $attribute) {
3940
/** @var Attr $attribute */
4041
$this->markBoundElement($element, $attribute, $key, $bindValue);
@@ -43,7 +44,7 @@ public function bind(
4344
}
4445

4546
$bindProperty = $this->getBindProperty($element, $attributeName);
46-
$modifier = $this->resolveModifier($key, $attribute->value);
47+
[$modifier, $remainingExpressions] = $this->resolveModifier($key, $attribute->value);
4748
if($modifier === false) {
4849
continue;
4950
}
@@ -57,10 +58,19 @@ public function bind(
5758
$this->appendDebugInfo($element, $bindProperty);
5859
$element->setAttribute("data-bound", "");
5960
if(!$attribute->ownerElement->hasAttribute("data-rebind")) {
60-
$attributesToRemove[] = $attributeName;
61+
if($remainingExpressions === []) {
62+
$attributesToRemove[] = $attributeName;
63+
}
64+
else {
65+
$attributeUpdates[$attributeName] = implode("; ", $remainingExpressions);
66+
}
6167
}
6268
}
6369

70+
foreach($attributeUpdates as $attributeName => $attributeValue) {
71+
$element->setAttribute($attributeName, $attributeValue);
72+
}
73+
6474
foreach($attributesToRemove as $attributeName) {
6575
$element->removeAttribute($attributeName);
6676
}
@@ -96,7 +106,7 @@ public function expandAttributes(Element $element):void {
96106
}
97107
}
98108

99-
private function normalizeBindValue(mixed $value):mixed {
109+
private function standardiseBindValue(mixed $value):mixed {
100110
if(is_scalar($value) || is_iterable($value)) {
101111
return $value;
102112
}
@@ -156,26 +166,54 @@ private function getBindProperty(
156166
return substr($attributeName, strpos($attributeName, ":") + 1);
157167
}
158168

169+
/** @return array{0:string|false|null, 1:array<int, string>} */
159170
private function resolveModifier(
160171
?string $key,
161172
string $attributeValue,
162-
):string|false|null {
173+
):array {
163174
if(is_null($key)) {
164-
return $attributeValue === ""
165-
? null
166-
: false;
175+
return [
176+
$attributeValue === ""
177+
? null
178+
: false,
179+
[],
180+
];
181+
}
182+
183+
$matchingExpression = null;
184+
$remainingExpressions = [];
185+
foreach($this->splitBindExpressions($attributeValue) as $expression) {
186+
$trimmedExpression = ltrim($expression, ":!?");
187+
$trimmedExpression = strtok($trimmedExpression, " ");
188+
$bindKey = $this->extractBindKey($trimmedExpression);
189+
if(is_null($matchingExpression) && ($key === $bindKey || $bindKey === "@")) {
190+
$matchingExpression = $expression;
191+
continue;
192+
}
193+
194+
$remainingExpressions[] = $expression;
167195
}
168196

169-
$trimmedAttrValue = ltrim($attributeValue, ":!?");
170-
$trimmedAttrValue = strtok($trimmedAttrValue, " ");
171-
$bindKey = $this->extractBindKey($trimmedAttrValue);
172-
if($key !== $bindKey && $bindKey !== "@") {
173-
return false;
197+
if(is_null($matchingExpression)) {
198+
return [false, []];
174199
}
175200

176-
return $attributeValue !== $trimmedAttrValue
177-
? $attributeValue
178-
: null;
201+
return [
202+
$matchingExpression !== ltrim($matchingExpression, ":!?")
203+
? $matchingExpression
204+
: null,
205+
$remainingExpressions,
206+
];
207+
}
208+
209+
/** @return array<int, string> */
210+
private function splitBindExpressions(string $attributeValue):array {
211+
return array_values(
212+
array_filter(
213+
array_map("trim", explode(";", $attributeValue)),
214+
fn(string $expression):bool => $expression !== "",
215+
),
216+
);
179217
}
180218

181219
private function defaultListBindingName(Element $element):string {
@@ -312,7 +350,9 @@ private function bindClassProperty(
312350
return;
313351
}
314352

315-
$element->classList->add($bindValue);
353+
foreach($this->prepareTokenListValues($bindValue) as $className) {
354+
$element->classList->add($className);
355+
}
316356
}
317357

318358
private function bindRemoveProperty(
@@ -350,8 +390,8 @@ private function handleModifier(
350390
string $modifier,
351391
mixed $bindValue
352392
):void {
353-
$modifierChar = $modifier[0];
354-
$modifierValue = substr($modifier, 1);
393+
$modifierChar = $this->getModifierType($modifier);
394+
$modifierValue = $this->getModifierBody($modifier);
355395
$condition = null;
356396
if(false !== $spacePos = strpos($modifierValue, " ")) {
357397
$modifierValue = substr($modifierValue, $spacePos + 1);
@@ -368,16 +408,20 @@ private function handleModifier(
368408
switch($modifierChar) {
369409
case ":":
370410
$tokenList = $this->getTokenList($element, $attribute);
411+
$tokenNames = $this->resolveTokenNames($modifier, $bindValue);
412+
if($this->isInverseModifier($modifier)) {
413+
$bindValue = !$bindValue;
414+
}
371415
if($bindValue) {
372-
$tokenList->add($modifierValue);
416+
$tokenList->add(...$tokenNames);
373417
}
374418
else {
375-
$tokenList->remove($modifierValue);
419+
$tokenList->remove(...$tokenNames);
376420
}
377421
break;
378422

379423
case "?":
380-
if($modifierValue[0] === "!") {
424+
if($this->isInverseModifier($modifier)) {
381425
$bindValue = !$bindValue;
382426
}
383427

@@ -422,16 +466,84 @@ private function extractBindKey(string $bindExpression):string {
422466
}
423467

424468
private function extractModifierExpression(string $modifier):string {
425-
$modifierValue = substr($modifier, 1);
426-
$modifierValue = ltrim($modifierValue, "!");
469+
$modifierValue = $this->getModifierBody($modifier);
427470
return strtok($modifierValue, " ") ?: "";
428471
}
429472

473+
/** @return array<int, string> */
474+
private function resolveTokenNames(string $modifier, mixed $bindValue):array {
475+
$tokenNames = $this->extractModifierTokens($modifier);
476+
if($tokenNames) {
477+
return $tokenNames;
478+
}
479+
480+
if(is_bool($bindValue)) {
481+
$bindExpression = $this->extractModifierExpression($modifier);
482+
return [$this->extractBindKey($bindExpression)];
483+
}
484+
485+
return $this->prepareTokenListValues($bindValue);
486+
}
487+
488+
/** @return array<int, string> */
489+
private function extractModifierTokens(string $modifier):array {
490+
$modifierValue = $this->getModifierBody($modifier);
491+
$spacePos = strpos($modifierValue, " ");
492+
if($spacePos === false) {
493+
return [];
494+
}
495+
496+
return $this->prepareTokenListValues(substr($modifierValue, $spacePos + 1));
497+
}
498+
499+
private function getModifierType(string $modifier):string {
500+
foreach(str_split($modifier) as $char) {
501+
if($char === ":" || $char === "?") {
502+
return $char;
503+
}
504+
}
505+
506+
return $modifier[0];
507+
}
508+
509+
private function isInverseModifier(string $modifier):bool {
510+
return str_contains($modifier, "!");
511+
}
512+
513+
private function getModifierBody(string $modifier):string {
514+
$modifierType = $this->getModifierType($modifier);
515+
$modifierValue = ltrim($modifier, "!");
516+
if(str_starts_with($modifierValue, $modifierType)) {
517+
return ltrim(substr($modifierValue, 1), "!");
518+
}
519+
520+
return ltrim(substr($modifier, 1), "!");
521+
}
522+
430523
private function extractCondition(string $bindExpression):?string {
431524
$parts = explode("=", $bindExpression, 2);
432525
return $parts[1] ?? null;
433526
}
434527

528+
/** @return array<int, string> */
529+
private function prepareTokenListValues(mixed $bindValue):array {
530+
if(is_iterable($bindValue)) {
531+
$tokenList = [];
532+
foreach($bindValue as $tokenValue) {
533+
array_push($tokenList, ...$this->prepareTokenListValues($tokenValue));
534+
}
535+
536+
return array_values(array_unique($tokenList));
537+
}
538+
539+
if(!is_scalar($bindValue) && !$bindValue instanceof \Stringable) {
540+
return [];
541+
}
542+
543+
$tokenList = preg_split('/\s+/', trim((string)$bindValue)) ?: [];
544+
return array_values(array_filter($tokenList, fn(string $token):bool => $token !== ""));
545+
}
546+
435547
private function valueMatchesCondition(mixed $bindValue, string $condition):bool {
436548
if(is_bool($bindValue)) {
437549
$bindValue = (int)$bindValue;

0 commit comments

Comments
 (0)