@@ -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 (
@@ -368,11 +408,12 @@ private function handleModifier(
368408 switch ($ modifierChar ) {
369409 case ": " :
370410 $ tokenList = $ this ->getTokenList ($ element , $ attribute );
411+ $ tokenNames = $ this ->resolveTokenNames ($ modifier , $ bindValue );
371412 if ($ bindValue ) {
372- $ tokenList ->add ($ modifierValue );
413+ $ tokenList ->add (... $ tokenNames );
373414 }
374415 else {
375- $ tokenList ->remove ($ modifierValue );
416+ $ tokenList ->remove (... $ tokenNames );
376417 }
377418 break ;
378419
@@ -427,11 +468,57 @@ private function extractModifierExpression(string $modifier):string {
427468 return strtok ($ modifierValue , " " ) ?: "" ;
428469 }
429470
471+ /** @return array<int, string> */
472+ private function resolveTokenNames (string $ modifier , mixed $ bindValue ):array {
473+ $ tokenNames = $ this ->extractModifierTokens ($ modifier );
474+ if ($ tokenNames ) {
475+ return $ tokenNames ;
476+ }
477+
478+ if (is_bool ($ bindValue )) {
479+ $ bindExpression = $ this ->extractModifierExpression ($ modifier );
480+ return [$ this ->extractBindKey ($ bindExpression )];
481+ }
482+
483+ return $ this ->prepareTokenListValues ($ bindValue );
484+ }
485+
486+ /** @return array<int, string> */
487+ private function extractModifierTokens (string $ modifier ):array {
488+ $ modifierValue = substr ($ modifier , 1 );
489+ $ modifierValue = ltrim ($ modifierValue , "! " );
490+ $ spacePos = strpos ($ modifierValue , " " );
491+ if ($ spacePos === false ) {
492+ return [];
493+ }
494+
495+ return $ this ->prepareTokenListValues (substr ($ modifierValue , $ spacePos + 1 ));
496+ }
497+
430498 private function extractCondition (string $ bindExpression ):?string {
431499 $ parts = explode ("= " , $ bindExpression , 2 );
432500 return $ parts [1 ] ?? null ;
433501 }
434502
503+ /** @return array<int, string> */
504+ private function prepareTokenListValues (mixed $ bindValue ):array {
505+ if (is_iterable ($ bindValue )) {
506+ $ tokenList = [];
507+ foreach ($ bindValue as $ tokenValue ) {
508+ array_push ($ tokenList , ...$ this ->prepareTokenListValues ($ tokenValue ));
509+ }
510+
511+ return array_values (array_unique ($ tokenList ));
512+ }
513+
514+ if (!is_scalar ($ bindValue ) && !$ bindValue instanceof \Stringable) {
515+ return [];
516+ }
517+
518+ $ tokenList = preg_split ('/\s+/ ' , trim ((string )$ bindValue )) ?: [];
519+ return array_values (array_filter ($ tokenList , fn (string $ token ):bool => $ token !== "" ));
520+ }
521+
435522 private function valueMatchesCondition (mixed $ bindValue , string $ condition ):bool {
436523 if (is_bool ($ bindValue )) {
437524 $ bindValue = (int )$ bindValue ;
0 commit comments