77 */
88
99import * as chars from '../chars' ;
10+ import {
11+ AST ,
12+ ASTWithSource ,
13+ ImplicitReceiver ,
14+ LiteralArray ,
15+ LiteralMap ,
16+ LiteralPrimitive ,
17+ PropertyRead ,
18+ RecursiveAstVisitor ,
19+ ThisReceiver ,
20+ } from '../expression_parser/ast' ;
1021import { Lexer , Token , TokenType } from '../expression_parser/lexer' ;
1122import * as html from '../ml_parser/ast' ;
1223import { ParseError , ParseSourceSpan } from '../parse_util' ;
@@ -117,6 +128,7 @@ export function parseWhenTrigger(
117128/** Parses an `on` trigger */
118129export function parseOnTrigger (
119130 { expression, sourceSpan} : html . BlockParameter ,
131+ bindingParser : BindingParser ,
120132 triggers : t . DeferredBlockTriggers ,
121133 errors : ParseError [ ] ,
122134 placeholder : t . DeferredBlockPlaceholder | null ,
@@ -135,16 +147,18 @@ export function parseOnTrigger(
135147 errors . push ( new ParseError ( sourceSpan , `Could not find "on" keyword in expression` ) ) ;
136148 } else {
137149 const start = getTriggerParametersStart ( expression , onIndex + 1 ) ;
150+ const isHydrationTrigger = expression . startsWith ( 'hydrate' ) ;
138151 const parser = new OnTriggerParser (
139152 expression ,
153+ bindingParser ,
140154 start ,
141155 sourceSpan ,
142156 triggers ,
143157 errors ,
144- expression . startsWith ( 'hydrate' )
158+ isHydrationTrigger
145159 ? validateHydrateReferenceBasedTrigger
146160 : validatePlainReferenceBasedTrigger ,
147- placeholder ,
161+ isHydrationTrigger ,
148162 prefetchSpan ,
149163 onSourceSpan ,
150164 hydrateSpan ,
@@ -173,12 +187,13 @@ class OnTriggerParser {
173187
174188 constructor (
175189 private expression : string ,
190+ private bindingParser : BindingParser ,
176191 private start : number ,
177192 private span : ParseSourceSpan ,
178193 private triggers : t . DeferredBlockTriggers ,
179194 private errors : ParseError [ ] ,
180195 private validator : ReferenceTriggerValidator ,
181- private placeholder : t . DeferredBlockPlaceholder | null ,
196+ private isHydrationTrigger : boolean ,
182197 private prefetchSpan : ParseSourceSpan | null ,
183198 private onSourceSpan : ParseSourceSpan ,
184199 private hydrateSpan : ParseSourceSpan | null ,
@@ -333,6 +348,9 @@ class OnTriggerParser {
333348 this . trackTrigger (
334349 'viewport' ,
335350 createViewportTrigger (
351+ this . start ,
352+ this . isHydrationTrigger ,
353+ this . bindingParser ,
336354 parameters ,
337355 nameSpan ,
338356 sourceSpan ,
@@ -568,6 +586,9 @@ function createInteractionTrigger(
568586}
569587
570588function createViewportTrigger (
589+ start : number ,
590+ isHydrationTrigger : boolean ,
591+ bindingParser : BindingParser ,
571592 parameters : ParsedParameter [ ] ,
572593 nameSpan : ParseSourceSpan ,
573594 sourceSpan : ParseSourceSpan ,
@@ -577,8 +598,74 @@ function createViewportTrigger(
577598 validator : ReferenceTriggerValidator ,
578599) : t . ViewportDeferredTrigger {
579600 validator ( OnTriggerType . VIEWPORT , parameters ) ;
601+
602+ let reference : string | null ;
603+ let options : LiteralMap | null ;
604+
605+ if ( parameters . length === 0 ) {
606+ reference = options = null ;
607+ } else if ( ! parameters [ 0 ] . expression . startsWith ( '{' ) ) {
608+ reference = parameters [ 0 ] . expression ;
609+ options = null ;
610+ } else {
611+ const parsed = bindingParser . parseBinding (
612+ parameters [ 0 ] . expression ,
613+ false ,
614+ sourceSpan ,
615+ sourceSpan . start . offset + start + parameters [ 0 ] . start ,
616+ ) ;
617+
618+ if ( ! ( parsed . ast instanceof LiteralMap ) ) {
619+ throw new Error ( 'Options parameter of the "viewport" trigger must be an object literal' ) ;
620+ } else if ( parsed . ast . keys . some ( ( key ) => key . key === 'root' ) ) {
621+ throw new Error (
622+ 'The "root" option is not supported in the options parameter of the "viewport" trigger' ,
623+ ) ;
624+ }
625+
626+ const triggerIndex = parsed . ast . keys . findIndex ( ( key ) => key . key === 'trigger' ) ;
627+
628+ if ( triggerIndex === - 1 ) {
629+ reference = null ;
630+ options = parsed . ast ;
631+ } else {
632+ const value = parsed . ast . values [ triggerIndex ] ;
633+ const triggerFilter = ( _ : unknown , index : number ) => index !== triggerIndex ;
634+
635+ if (
636+ ! ( value instanceof PropertyRead ) ||
637+ ! ( value . receiver instanceof ImplicitReceiver ) ||
638+ value . receiver instanceof ThisReceiver
639+ ) {
640+ throw new Error ( `"trigger" option of the "viewport" trigger must be an identifier` ) ;
641+ }
642+
643+ reference = ( value as PropertyRead ) . name ;
644+ options = new LiteralMap (
645+ parsed . ast . span ,
646+ parsed . ast . sourceSpan ,
647+ parsed . ast . keys . filter ( triggerFilter ) ,
648+ parsed . ast . values . filter ( triggerFilter ) ,
649+ ) ;
650+ }
651+ }
652+
653+ if ( isHydrationTrigger && reference !== null ) {
654+ throw new Error ( `"viewport" hydration trigger cannot have a "trigger"` ) ;
655+ } else if ( options ) {
656+ const dynamicNode = DynamicAstValidator . findDynamicNode ( options ) ;
657+
658+ if ( dynamicNode !== null ) {
659+ throw new Error (
660+ `Options of the "viewport" trigger must be an object ` +
661+ `literal containing only literal values, but "${ dynamicNode . constructor . name } " was found` ,
662+ ) ;
663+ }
664+ }
665+
580666 return new t . ViewportDeferredTrigger (
581- parameters [ 0 ] ?. expression ?? null ,
667+ reference ,
668+ options ,
582669 nameSpan ,
583670 sourceSpan ,
584671 prefetchSpan ,
@@ -604,6 +691,13 @@ function validatePlainReferenceBasedTrigger(type: OnTriggerType, parameters: Par
604691 * @param parameters Parameters of the trigger.
605692 */
606693function validateHydrateReferenceBasedTrigger ( type : OnTriggerType , parameters : ParsedParameter [ ] ) {
694+ if ( type === OnTriggerType . VIEWPORT ) {
695+ if ( parameters . length > 1 ) {
696+ throw new Error ( `Hydration trigger "${ type } " cannot have more than one parameter` ) ;
697+ }
698+ return ;
699+ }
700+
607701 if ( parameters . length > 0 ) {
608702 throw new Error ( `Hydration trigger "${ type } " cannot have parameters` ) ;
609703 }
@@ -638,3 +732,26 @@ export function parseDeferredTime(value: string): number | null {
638732 const [ time , units ] = match ;
639733 return parseFloat ( time ) * ( units === 's' ? 1000 : 1 ) ;
640734}
735+
736+ class DynamicAstValidator extends RecursiveAstVisitor {
737+ private dynamicNode : AST | null = null ;
738+
739+ static findDynamicNode ( ast : AST ) : AST | null {
740+ const visitor = new DynamicAstValidator ( ) ;
741+ visitor . visit ( ast ) ;
742+ return visitor . dynamicNode ;
743+ }
744+
745+ override visit ( ast : AST ) : void {
746+ if (
747+ ! ( ast instanceof ASTWithSource ) &&
748+ ! ( ast instanceof LiteralPrimitive ) &&
749+ ! ( ast instanceof LiteralArray ) &&
750+ ! ( ast instanceof LiteralMap )
751+ ) {
752+ this . dynamicNode = ast ;
753+ } else {
754+ super . visit ( ast ) ;
755+ }
756+ }
757+ }
0 commit comments