@@ -84,6 +84,7 @@ let activeSub: ReactiveNode | undefined
8484const startBatch = ( ) : void => {
8585 batchDepth ++
8686}
87+
8788const endBatch = ( ) : void => {
8889 if ( ! -- batchDepth ) {
8990 flush ( )
@@ -576,19 +577,17 @@ const isValidLink = (checkLink: Link, sub: ReactiveNode): boolean => {
576577 return false
577578}
578579
579- const getPath = < T = any > ( path : string ) : T =>
580- path . split ( '.' ) . reduce ( ( acc , key ) => acc [ key ] , root ) as T
581-
582- const hasPath = ( path : string ) : boolean =>
583- peek (
584- ( ) =>
585- path
586- . split ( '.' )
587- . reduce (
588- ( obj , key ) => ( obj && Object . hasOwn ( obj , key ) ? obj [ key ] : undefined ) ,
589- root ,
590- ) !== undefined ,
591- )
580+ const getPath = < T = any > ( path : string ) : T | undefined => {
581+ let result = root
582+ const split = path . split ( '.' )
583+ for ( const path of split ) {
584+ if ( result == null || ! Object . hasOwn ( result , path ) ) {
585+ return
586+ }
587+ result = result [ path ]
588+ }
589+ return result as T
590+ }
592591
593592export const DELETE = Symbol ( 'delete' )
594593const deep = ( value : any , prefix = '' ) : any => {
@@ -608,6 +607,9 @@ const deep = (value: any, prefix = ''): any => {
608607 keys ( )
609608 return deepObj [ prop ]
610609 } else {
610+ if ( typeof prop === 'symbol' ) {
611+ return deepObj [ prop ]
612+ }
611613 if ( ! Object . hasOwn ( deepObj , prop ) || deepObj [ prop ] ( ) == null ) {
612614 deepObj [ prop ] = signal ( '' )
613615 dispatch ( { [ prefix + prop ] : '' } )
@@ -767,7 +769,10 @@ function filtered(
767769 for ( const key in node ) {
768770 if ( isPojo ( node [ key ] ) ) {
769771 stack . push ( [ node [ key ] , `${ prefix + key } .` ] )
770- } else if ( include . test ( prefix + key ) && ! exclude . test ( prefix + key ) ) {
772+ } else if (
773+ toRegExp ( include ) . test ( prefix + key ) &&
774+ ! toRegExp ( exclude ) . test ( prefix + key )
775+ ) {
771776 pathObj [ prefix + key ] = getPath ( prefix + key )
772777 }
773778 }
@@ -776,6 +781,14 @@ function filtered(
776781 return pathToObj ( { } , pathObj )
777782}
778783
784+ function toRegExp ( val : string | RegExp ) : RegExp {
785+ if ( typeof val === 'string' ) {
786+ return RegExp ( val . replace ( / ^ \/ | \/ $ / g, '' ) )
787+ }
788+
789+ return val
790+ }
791+
779792const root : Record < string , any > = deep ( { } )
780793
781794/**
@@ -813,10 +826,12 @@ export function load(...pluginsToLoad: DatastarPlugin[]) {
813826 mergePatch,
814827 peek,
815828 getPath,
816- hasPath,
817829 startBatch,
818830 endBatch,
831+ initErr : 0 as any ,
819832 }
833+ ctx . initErr = initErr . bind ( 0 , ctx )
834+
820835 if ( plugin . type === 'action' ) {
821836 actions [ plugin . name ] = plugin
822837 } else if ( plugin . type === 'attribute' ) {
@@ -825,7 +840,7 @@ export function load(...pluginsToLoad: DatastarPlugin[]) {
825840 } else if ( plugin . type === 'watcher' ) {
826841 plugin . onGlobalInit ?.( ctx )
827842 } else {
828- throw initErr ( 'InvalidPluginType' , ctx )
843+ throw ctx . initErr ( 'InvalidPluginType' )
829844 }
830845 }
831846
@@ -850,6 +865,19 @@ function applyEls(els: Iterable<HTMLOrSVG>): void {
850865 }
851866}
852867
868+ function cleanupEls ( els : Iterable < HTMLOrSVG > ) : void {
869+ for ( const el of els ) {
870+ const cleanups = removals . get ( el )
871+ // If removals has el, delete it and run all cleanup functions
872+ if ( removals . delete ( el ) ) {
873+ for ( const cleanup of cleanups ! . values ( ) ) {
874+ cleanup ( )
875+ }
876+ cleanups ! . clear ( )
877+ }
878+ }
879+ }
880+
853881// Apply all plugins to the entire DOM or a provided element
854882export function apply ( root : HTMLOrSVG = document . body ) {
855883 // Delay applying plugins to give custom plugins a chance to load
@@ -875,90 +903,96 @@ function applyAttributePlugin(
875903 attrKey : string ,
876904 value : string ,
877905) : void {
878- const rawKey = camel ( alias ? attrKey . slice ( alias . length ) : attrKey )
879- const plugin = plugins . find ( ( _ , i ) => pluginRegexs [ i ] . test ( rawKey ) )
880- if ( plugin ) {
881- // Extract the key and modifiers
882- let [ key , ...rawModifiers ] = rawKey . slice ( plugin . name . length ) . split ( / _ _ + / )
883-
884- const hasKey = ! ! key
885- if ( hasKey ) {
886- key = camel ( key )
887- }
888- const hasValue = ! ! value
889-
890- // Create the runtime context
891- const ctx : RuntimeContext = {
892- plugin,
893- actions,
894- root,
895- filtered,
896- signal,
897- computed,
898- effect,
899- mergePatch,
900- peek,
901- getPath,
902- hasPath,
903- startBatch,
904- endBatch,
905- el,
906- rawKey,
907- key,
908- value,
909- mods : new Map ( ) ,
910- runtimeErr : 0 as any ,
911- rx : 0 as any ,
912- }
913- ctx . runtimeErr = runtimeErr . bind ( 0 , ctx )
914- if ( plugin . shouldEvaluate === undefined || plugin . shouldEvaluate === true ) {
915- ctx . rx = generateReactiveExpression ( ctx )
916- }
917-
918- // Check the requirements
919- const keyReq = plugin . keyReq || 'allowed'
920- if ( hasKey ) {
921- if ( keyReq === 'denied' ) {
922- throw ctx . runtimeErr ( `${ plugin . name } KeyNotAllowed` )
906+ if ( attrKey . startsWith ( alias ) ) {
907+ const rawKey = camel ( alias ? attrKey . slice ( alias . length ) : attrKey )
908+ const plugin = plugins . find ( ( _ , i ) => pluginRegexs [ i ] . test ( rawKey ) )
909+ if ( plugin ) {
910+ // Extract the key and modifiers
911+ let [ key , ...rawModifiers ] = rawKey . slice ( plugin . name . length ) . split ( / _ _ + / )
912+
913+ const hasKey = ! ! key
914+ if ( hasKey ) {
915+ key = camel ( key )
916+ }
917+ const hasValue = ! ! value
918+
919+ // Create the runtime context
920+ const ctx : RuntimeContext = {
921+ plugin,
922+ actions,
923+ root,
924+ filtered,
925+ signal,
926+ computed,
927+ effect,
928+ mergePatch,
929+ peek,
930+ getPath,
931+ startBatch,
932+ endBatch,
933+ initErr : 0 as any ,
934+ el,
935+ rawKey,
936+ key,
937+ value,
938+ mods : new Map ( ) ,
939+ runtimeErr : 0 as any ,
940+ rx : 0 as any ,
941+ }
942+ ctx . initErr = initErr . bind ( 0 , ctx )
943+ ctx . runtimeErr = runtimeErr . bind ( 0 , ctx )
944+ if (
945+ plugin . shouldEvaluate === undefined ||
946+ plugin . shouldEvaluate === true
947+ ) {
948+ ctx . rx = generateReactiveExpression ( ctx )
923949 }
924- } else if ( keyReq === 'must' ) {
925- throw ctx . runtimeErr ( `${ plugin . name } KeyRequired` )
926- }
927950
928- const valReq = plugin . valReq || 'allowed'
929- if ( hasValue ) {
930- if ( valReq === 'denied' ) {
931- throw ctx . runtimeErr ( `${ plugin . name } ValueNotAllowed` )
951+ // Check the requirements
952+ const keyReq = plugin . keyReq || 'allowed'
953+ if ( hasKey ) {
954+ if ( keyReq === 'denied' ) {
955+ throw ctx . runtimeErr ( `${ plugin . name } KeyNotAllowed` )
956+ }
957+ } else if ( keyReq === 'must' ) {
958+ throw ctx . runtimeErr ( `${ plugin . name } KeyRequired` )
932959 }
933- } else if ( valReq === 'must' ) {
934- throw ctx . runtimeErr ( `${ plugin . name } ValueRequired` )
935- }
936960
937- // Check for exclusive requirements
938- if ( keyReq === 'exclusive' || valReq === 'exclusive' ) {
939- if ( hasKey && hasValue ) {
940- throw ctx . runtimeErr ( `${ plugin . name } KeyAndValueProvided` )
961+ const valReq = plugin . valReq || 'allowed'
962+ if ( hasValue ) {
963+ if ( valReq === 'denied' ) {
964+ throw ctx . runtimeErr ( `${ plugin . name } ValueNotAllowed` )
965+ }
966+ } else if ( valReq === 'must' ) {
967+ throw ctx . runtimeErr ( `${ plugin . name } ValueRequired` )
941968 }
942- if ( ! hasKey && ! hasValue ) {
943- throw ctx . runtimeErr ( `${ plugin . name } KeyOrValueRequired` )
969+
970+ // Check for exclusive requirements
971+ if ( keyReq === 'exclusive' || valReq === 'exclusive' ) {
972+ if ( hasKey && hasValue ) {
973+ throw ctx . runtimeErr ( `${ plugin . name } KeyAndValueProvided` )
974+ }
975+ if ( ! hasKey && ! hasValue ) {
976+ throw ctx . runtimeErr ( `${ plugin . name } KeyOrValueRequired` )
977+ }
944978 }
945- }
946979
947- for ( const rawMod of rawModifiers ) {
948- const [ label , ...mod ] = rawMod . split ( '.' )
949- ctx . mods . set ( camel ( label ) , new Set ( mod . map ( ( t ) => t . toLowerCase ( ) ) ) )
950- }
980+ for ( const rawMod of rawModifiers ) {
981+ const [ label , ...mod ] = rawMod . split ( '.' )
982+ ctx . mods . set ( camel ( label ) , new Set ( mod . map ( ( t ) => t . toLowerCase ( ) ) ) )
983+ }
951984
952- const cleanup = plugin . onLoad ( ctx )
953- if ( cleanup ) {
954- let cleanups = removals . get ( el )
955- if ( cleanups ) {
956- cleanups . get ( rawKey ) ?.( )
957- } else {
958- cleanups = new Map ( )
959- removals . set ( el , cleanups )
985+ const cleanup = plugin . onLoad ( ctx )
986+ if ( cleanup ) {
987+ let cleanups = removals . get ( el )
988+ if ( cleanups ) {
989+ cleanups . get ( rawKey ) ?.( )
990+ } else {
991+ cleanups = new Map ( )
992+ removals . set ( el , cleanups )
993+ }
994+ cleanups . set ( rawKey , cleanup )
960995 }
961- cleanups . set ( rawKey , cleanup )
962996 }
963997 }
964998}
@@ -977,14 +1011,8 @@ function observe(mutations: MutationRecord[]) {
9771011 if ( type === 'childList' ) {
9781012 for ( const node of removedNodes ) {
9791013 if ( isHTMLOrSVG ( node ) ) {
980- const cleanups = removals . get ( node )
981- // If removals has el, delete it and run all cleanup functions
982- if ( removals . delete ( node ) ) {
983- for ( const cleanup of cleanups ! . values ( ) ) {
984- cleanup ( )
985- }
986- cleanups ! . clear ( )
987- }
1014+ cleanupEls ( [ node ] )
1015+ cleanupEls ( node . querySelectorAll < HTMLOrSVG > ( '*' ) )
9881016 }
9891017 }
9901018
@@ -1072,13 +1100,17 @@ function generateReactiveExpression(
10721100 // $['foo'] → $['foo']
10731101 // $foo[obj.bar] → $['foo'][obj.bar]
10741102 // $foo['bar.baz'] → $['foo']['bar.baz']
1103+ // $1 → $['1']
1104+ // $123 → $['123']
1105+ // $foo.0.name → $['foo']['0']['name']
1106+ // $foo.0.1.2.bar.0 → $['foo']['0']['1']['2']['bar']['0']
10751107
10761108 // Transform all signal patterns
10771109 expr = expr
10781110 // $['x'] → $x (normalize existing bracket notation)
1079- . replace ( / \$ \[ ' ( [ a - z A - Z _ $ ] [ \w $ ] * ) ' \] / g, '$$$1' )
1111+ . replace ( / \$ \[ ' ( [ a - z A - Z _ $ \d ] [ \w $ ] * ) ' \] / g, '$$$1' )
10801112 // $x → $['x'] (including dots and hyphens)
1081- . replace ( / \$ ( [ a - z A - Z _ ] \w * (?: [ . - ] \w + ) * ) / g, ( _ , signalName ) => {
1113+ . replace ( / \$ ( [ a - z A - Z _ \d ] \w * (?: [ . - ] \w + ) * ) / g, ( _ , signalName ) => {
10821114 const parts = signalName . split ( '.' )
10831115 return parts . reduce (
10841116 ( acc : string , part : string ) => `${ acc } ['${ part } ']` ,
@@ -1087,7 +1119,7 @@ function generateReactiveExpression(
10871119 } )
10881120 // $ inside brackets: [$x] → [$['x']]
10891121 . replace (
1090- / \[ ( \$ [ a - z A - Z _ ] \w * ) \] / g,
1122+ / \[ ( \$ [ a - z A - Z _ \d ] \w * ) \] / g,
10911123 ( _ , varName ) => `[$['${ varName . slice ( 1 ) } ']]` ,
10921124 )
10931125
0 commit comments