@@ -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