import { render as renderLit , Template , nothing } from 'lit-html' ; import { CompiledTemplate , CompiledTemplateResult } from 'lit-html' ; import { parse , Parser , EvalAstFactory } from 'jexpr' ; import type { Expression , Scope } from 'jexpr/lib/eval' ; import { _Σ } from 'lit-html/private-ssr-support.js' ; const { AttributePart , PropertyPart , BooleanAttributePart , EventPart } = _Σ ; const astFactory = new EvalAstFactory ( ) ; const expressionCache = new Map < string , Expression | undefined > ( ) ; const toCamelCase = ( s ) => s . replace ( /-(-|\w)/g , ( _ , p1 ) => p1 . toUpperCase ( ) ) ; /**
 * Gets the value from a string that contains a delimted expression: {{ ... }}
 */ const getSingleValue = ( s , model ) => { let ast = expressionCache . get ( s ) ; if ( ast === undefined ) { if ( expressionCache . has ( s ) ) { return undefined ; } s = s . trim ( ) ; if ( s . startsWith ( '{{' ) && s . endsWith ( '}}' ) ) { const expression = s . substring ( 2 , s . length - 2 ) . trim ( ) ; ast = new Parser ( expression , astFactory ) . parse ( ) ; expressionCache . set ( s , ast ) ; } } return ast ?. evaluate ( model ) ; } ; export interface TemplateFunction { ( model ) : unknown ; } /**
 * A Renderer is responsible for rendering a block call, like
 * <template name="foo">
 */ export interface Renderer { ( model , handlers , renderers ) : unknown ; } export interface Renderers { [ name ] : Renderer ; } /**
 * A TemplateHandlers is responsible for rendering control flow like
 * <template type="if" if="{{x}}">
 */ export type TemplateHandler = ( template : HTMLTemplateElement , model : object , handlers : TemplateHandlers , renderers : Renderers ) => unknown ; export interface TemplateHandlers { [ name ] : TemplateHandler ; } export const ifHandler = ( template , model , handlers , renderers ) => { const ifAttribute = template . getAttribute ( 'if' ) ; if ( ifAttribute !== null && getSingleValue ( ifAttribute , model ) ) { return evaluateTemplate ( template , model , handlers , renderers ) ; } return undefined ; } ; export const repeatHandler = ( template , model , handlers , renderers ) => { const repeatAttribute = template . getAttribute ( 'repeat' ) ; if ( repeatAttribute !== null ) { const items = getSingleValue ( repeatAttribute , model ) ; if ( ! items [ Symbol . iterator ] ) { return nothing ; } const litTemplate = getLitTemplate ( template ) ; let index = - 1 ; const result = [ ] ; for ( const item of items ) { index ++ ; const itemModel = Object . create ( model ) ; itemModel . item = item ; itemModel . index = index ; itemModel [ 'this' ] = model [ 'this' ] ?? model ; const values = litTemplate . parts . map ( ( part ) => part . update ( itemModel , handlers , renderers ) ) ; const templateResult = { _$litType$ : litTemplate , values , } ; result . push ( templateResult ) ; } return result ; } return undefined ; } ; export const defaultHandlers = < TemplateHandlers > { if : ifHandler , repeat : repeatHandler , } ; returns { Function } export const prepareTemplate = ( template , handlers = defaultHandlers , renderers = { } , superTemplate ? ) : TemplateFunction => { const litTemplate = getLitTemplate ( template ) ; const templateRenderers = litTemplate . renderers ; if ( superTemplate ) { const superLitTemplate = getLitTemplate ( superTemplate ) ; const superRenderers = superLitTemplate . renderers ; const superCallRenderer = templateRenderers [ 'super' ] ; if ( superCallRenderer !== undefined ) { renderers = { ... templateRenderers , ... renderers , super : ( model , handlers , renderers ) => { renderers = { ... superRenderers , ... renderers , super : ( model , handlers , renderers ) => { return evaluateTemplate ( superTemplate , model , handlers , renderers ) ; } , } ; return superCallRenderer ( model , handlers , renderers ) ; } , } ; } else { renderers = { ... superRenderers , ... templateRenderers , ... renderers , } ; template = superTemplate ; } } else { renderers = { ... renderers , ... templateRenderers , } ; } return ( model ) => evaluateTemplate ( template , model , handlers , renderers ) ; } ; export interface RenderOptions { renderers ? : Renderers ; extends ? : HTMLTemplateElement ; } /**
 * Renders a template element containing a Stampino template.
 *
 * This is a convenience function wrapper around:
 *
 * ```
 * import {render} from 'lit';
 * const templateFn = prepareTemplate(templateEl);
 * render(templateFn(model), container);
 * ```
 */ export const render = ( template , container , model , handlers = defaultHandlers ) => { const litTemplate = prepareTemplate ( template , handlers ) ; renderLit ( litTemplate ( model ) , container ) ; } ; param template param model param handlers param renderers returns export const evaluateTemplate = ( template , model , handlers = defaultHandlers , renderers = { } ) => { const litTemplate = getLitTemplate ( template ) ; const values = [ ] ; for ( const part of litTemplate . parts ) { const value = part . update ( model , handlers , renderers ) ; if ( part . type === 1 ) { values . push ( ... ( value as Iterable < unknown > ) ) ; } else { values . push ( value ) ; } } const templateResult = { _$litType$ : litTemplate , values , } ; return templateResult ; } ; type TemplatePart = Template [ 'parts' ] [ 0 ] ; type StampinoTemplatePart = TemplatePart & { update : PartUpdater ; } ; type PartUpdater = ( model : object , handlers : TemplateHandlers , blocks : Renderers ) => unknown ; interface StampinoTemplate extends CompiledTemplate { parts : Array < StampinoTemplatePart > ; renderers : Renderers ; } const litTemplateCache = new Map < HTMLTemplateElement , StampinoTemplate > ( ) ; export const getLitTemplate = ( template ) : StampinoTemplate => { let litTemplate = litTemplateCache . get ( template ) ; if ( litTemplate === undefined ) { litTemplateCache . set ( template , ( litTemplate = makeLitTemplate ( template ) ) ) ; } return litTemplate ; } ; const makeLitTemplate = ( template ) : StampinoTemplate => { const litTemplate = { h : ( undefined as unknown ) as TrustedHTML , el : template . cloneNode ( true ) as HTMLTemplateElement , parts : [ ] , renderers : { } , } ; const walker = document . createTreeWalker ( litTemplate . el ! . content , NodeFilter . SHOW_ELEMENT | NodeFilter . SHOW_TEXT | NodeFilter . SHOW_COMMENT ) ; let node = walker . currentNode ; let nodeIndex = - 1 ; const elementsToRemove = [ ] ; while ( ( node = walker . nextNode ( ) ) !== null ) { if ( node . nodeType === Node . ELEMENT_NODE ) { nodeIndex ++ ; const element = node as Element ; if ( element . tagName === 'TEMPLATE' ) { const type = element . getAttribute ( 'type' ) ; const name = element . getAttribute ( 'name' ) ; if ( type !== null || name !== null ) { element . parentNode ! . insertBefore ( document . createComment ( '' ) , element ) ; elementsToRemove . push ( element ) ; let update ; if ( type !== null ) { update = ( model , handlers , renderers ) => { const handler = handlers [ type ] ; return handler ?. ( element as HTMLTemplateElement , model , handlers , renderers ) ; } ; } else { if ( name === 'super' ) { litTemplate . renderers [ 'super' ] = ( model , handlers , renderers ) => { const superRenderer = renderers [ 'super' ] ; const superCallTemplate = getLitTemplate ( element as HTMLTemplateElement ) ; renderers = { ... renderers , ... superCallTemplate . renderers , } ; return superRenderer ( model , handlers , renderers ) ; } ; } else { litTemplate . renderers [ name ! ] = ( model , handlers , renderers ) => { return evaluateTemplate ( element as HTMLTemplateElement , model , handlers , renderers ) ; } ; } update = ( model , handlers , renderers ) => { const renderer = renderers [ name ! ] ; return renderer ?. ( model , handlers , renderers ) ; } ; } litTemplate . parts . push ( { type : 2 , index : nodeIndex , update , } ) ; } } else { const attributeNames = element . getAttributeNames ( ) ; for ( const attributeName of attributeNames ) { const attributeValue = element . getAttribute ( attributeName ) ! ; const splitValue = attributeValue . split ( /(?<!\\){{(.*?)(?:(?<!\\)}})/g ) ; if ( splitValue . length === 1 ) { continue ; } element . removeAttribute ( attributeName ) ; let name = attributeName ; let ctor = AttributePart ; const prefix = attributeName [ 0 ] ; if ( prefix === '.' ) { name = toCamelCase ( attributeName . substring ( 1 ) ) ; ctor = PropertyPart ; } else if ( prefix === '?' ) { name = attributeName . substring ( 1 ) ; ctor = BooleanAttributePart ; } else if ( prefix === '@' ) { name = toCamelCase ( attributeName . substring ( 1 ) ) ; ctor = EventPart ; } const strings = [ splitValue [ 0 ] ] ; const exprs = [ ] ; for ( let i = 1 ; i < splitValue . length ; i += 2 ) { const exprText = splitValue [ i ] ; exprs . push ( parse ( exprText , astFactory ) as Expression ) ; strings . push ( splitValue [ i + 1 ] ) ; } litTemplate . parts . push ( { type : 1 , index : nodeIndex , name , strings , ctor , update : ( model , _handlers , _renderers ) => { return exprs . map ( ( expr ) => expr . evaluate ( model ) ) ; } , } ) ; } } } else if ( node . nodeType === Node . TEXT_NODE ) { const textNode = node as Text ; const text = textNode . textContent ! ; const strings = text . split ( /(?<!\\){{(.*?)(?:(?<!\\)}})/g ) ; if ( strings . length > 1 ) { textNode . textContent = strings [ 0 ] . replace ( '\\{{' , '{{' ) ; } else { textNode . textContent = text . replace ( '\\{{' , '{{' ) ; } for ( let i = 1 ; i < strings . length ; i += 2 ) { const exprText = strings [ i ] ; const expr = parse ( exprText , astFactory ) as Expression ; litTemplate . parts . push ( { type : 2 , index : ++ nodeIndex , update : ( model , _handlers ) => expr . evaluate ( model as Scope ) , } ) ; const newTextNode = new Text ( strings [ i + 1 ] . replace ( '\\{{' , '{{' ) ) ; textNode . parentNode ! . insertBefore ( newTextNode , textNode . nextSibling ) ; textNode . parentNode ! . insertBefore ( document . createComment ( '' ) , textNode . nextSibling ) ; walker . currentNode = newTextNode ; } } } for ( const e of elementsToRemove ) { e . remove ( ) ; } return litTemplate ; } ;