@ -1,56 +1,24 @@
/ * *
/ * *
* @ import { TemplateOperations } from "../types.js"
* @ import { TemplateOperations } from "../types.js"
* @ import { Namespace } from "#compiler"
* @ import { Namespace } from "#compiler"
* @ import { CallExpression , Statement } from "estree"
* @ import { CallExpression , Statement , ObjectExpression , Identifier , ArrayExpression , Property , Expression , Literal } from "estree"
* /
* /
import { NAMESPACE _SVG , NAMESPACE _MATHML } from '../../../../../constants.js' ;
import { NAMESPACE _SVG , NAMESPACE _MATHML } from '../../../../../constants.js' ;
import * as b from '../../../../utils/builders.js' ;
import * as b from '../../../../utils/builders.js' ;
import { regex _is _valid _identifier } from '../../../patterns.js' ;
import fix _attribute _casing from './fix-attribute-casing.js' ;
import fix _attribute _casing from './fix-attribute-casing.js' ;
class Scope {
declared = new Map ( ) ;
/ * *
* @ param { string } _name
* /
generate ( _name ) {
let name = _name . replace ( /[^a-zA-Z0-9_$]/g , '_' ) . replace ( /^[0-9]/ , '_' ) ;
if ( ! this . declared . has ( name ) ) {
this . declared . set ( name , 1 ) ;
return name ;
}
let count = this . declared . get ( name ) ;
this . declared . set ( name , count + 1 ) ;
return ` ${ name } _ ${ count } ` ;
}
}
/ * *
/ * *
* @ param { TemplateOperations } items
* @ param { TemplateOperations } items
* @ param { Namespace } namespace
* /
* /
export function template _to _functions ( items , namespace ) {
export function template _to _functions ( items ) {
let elements = [ ] ;
let elements = b . array ( [ ] ) ;
let body = [ ] ;
let scope = new Scope ( ) ;
/ * *
/ * *
* @ type { Array < Element > }
* @ type { Array < Element > }
* /
* /
let elements _stack = [ ] ;
let elements _stack = [ ] ;
/ * *
* @ type { Array < string > }
* /
let namespace _stack = [ ] ;
/ * *
* @ type { number }
* /
let foreign _object _count = 0 ;
/ * *
/ * *
* @ type { Element | undefined }
* @ type { Element | undefined }
* /
* /
@ -71,89 +39,48 @@ export function template_to_functions(items, namespace) {
// we closed one element, we remove it from the stack and eventually revert back
// we closed one element, we remove it from the stack and eventually revert back
// the namespace to the previous one
// the namespace to the previous one
if ( instruction . kind === 'pop_element' ) {
if ( instruction . kind === 'pop_element' ) {
const removed = elements _stack . pop ( ) ;
elements _stack . pop ( ) ;
if ( removed ? . namespaced ) {
namespace _stack . pop ( ) ;
}
if ( removed ? . element === 'foreignObject' ) {
foreign _object _count -- ;
}
continue ;
continue ;
}
}
// if the inserted node is in the svg/mathml we push the namespace to the stack because we need to
// create with createElementNS
if ( instruction . metadata ? . svg || instruction . metadata ? . mathml ) {
namespace _stack . push ( instruction . metadata . svg ? NAMESPACE _SVG : NAMESPACE _MATHML ) ;
}
// @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that
// @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that
const value = map [ instruction . kind ] (
const value = map [ instruction . kind ] (
... [
... [
// for set prop we need to send the last element (not the one in the stack since
// it get's added to the stack only after the push_element instruction)...for all the rest
// the first prop is a the scope to generate the name of the variable
... ( instruction . kind === 'set_prop' ? [ last _current _element ] : [ scope ] ) ,
// for create element we also need to add the namespace...namespaces in the stack get's precedence over
// the "global" namespace (and if we are in a foreignObject we default to html)
... ( instruction . kind === 'create_element'
... ( instruction . kind === 'create_element'
? [
? [ ]
foreign _object _count > 0
: [ instruction . kind === 'set_prop' ? last _current _element : elements _stack . at ( - 1 ) ] ) ,
? undefined
: namespace _stack . at ( - 1 ) ? ?
( namespace === 'svg'
? NAMESPACE _SVG
: namespace === 'mathml'
? NAMESPACE _MATHML
: undefined )
]
: [ ] ) ,
... ( instruction . args ? ? [ ] )
... ( instruction . args ? ? [ ] )
]
]
) ;
) ;
if ( value ) {
// this will compose the body of the function
body . push ( value . call ) ;
}
// with set_prop we don't need to do anything else, in all other cases we also need to
// with set_prop we don't need to do anything else, in all other cases we also need to
// append the element/node/anchor to the current active element or push it in the elements array
// append the element/node/anchor to the current active element or push it in the elements array
if ( instruction . kind !== 'set_prop' ) {
if ( instruction . kind !== 'set_prop' ) {
if ( elements _stack . length >= 1 && value ) {
if ( elements _stack . length >= 1 && value !== undefined ) {
const { call } = map . insert ( /** @type {Element} */ ( elements _stack . at ( - 1 ) ) , value ) ;
map . insert ( /** @type {Element} */ ( elements _stack . at ( - 1 ) ) , value ) ;
body . push ( call ) ;
} else if ( value !== undefined ) {
} else if ( value ) {
elements . elements . push ( value ) ;
elements . push ( b . id ( value . name ) ) ;
}
}
// keep track of the last created element (it will be pushed to the stack after the props are set)
// keep track of the last created element (it will be pushed to the stack after the props are set)
if ( instruction . kind === 'create_element' ) {
if ( instruction . kind === 'create_element' ) {
last _current _element = /** @type {Element} */ ( value ) ;
last _current _element = /** @type {Element} */ ( value ) ;
if ( last _current _element . element === 'foreignObject' ) {
foreign _object _count ++ ;
}
}
}
}
}
}
}
// every function needs to return a fragment so we create one and push all the elements there
const fragment = scope . generate ( 'fragment' ) ;
body . push ( b . var ( fragment , b . call ( 'document.createDocumentFragment' ) ) ) ;
body . push ( b . call ( fragment + '.append' , ... elements ) ) ;
body . push ( b . return ( b . id ( fragment ) ) ) ;
return b. arrow ( [ ] , b . block ( body ) ) ;
return elements ;
}
}
/ * *
/ * *
* @ typedef { { call : Statement , name : string , add _is : ( value : string ) => void , namespaced : boolean ; element : string ; } } Element
* @ typedef { ObjectExpression } Element
* /
* /
/ * *
/ * *
* @ typedef { { call : Statement , name : string } } Anchor
* @ typedef { void | null | ArrayExpression } Anchor
* /
* /
/ * *
/ * *
* @ typedef { { call : Statement , name : string } } Text
* @ typedef { void | Literal } Text
* /
* /
/ * *
/ * *
@ -161,109 +88,89 @@ export function template_to_functions(items, namespace) {
* /
* /
/ * *
/ * *
* @ param { Scope } scope
* @ param { Namespace } namespace
* @ param { string } element
* @ param { string } element
* @ returns { Element }
* @ returns { Element }
* /
* /
function create _element ( scope , namespace , element ) {
function create _element ( element ) {
const name = scope . generate ( element ) ;
return b . object ( [ b . prop ( 'init' , b . id ( 'e' ) , b . literal ( element ) ) ] ) ;
let fn = namespace != null ? 'document.createElementNS' : 'document.createElement' ;
}
let args = [ b . literal ( element ) ] ;
if ( namespace != null ) {
/ * *
args . unshift ( b . literal ( namespace ) ) ;
*
}
* @ param { Element } element
const call = b . var ( name , b . call ( fn , ... args ) ) ;
* @ param { string } name
/ * *
* @ param { Expression } init
* if there 's an "is" attribute we can' t just add it as a property , it needs to be
* @ returns { Property }
* specified on creation like this ` document.createElement('button', { is: 'my-button' }) `
* /
*
function get _or _create _prop ( element , name , init ) {
* Since the props are appended after the creation we change the generated call arguments and we push
let prop = element . properties . find (
* the is attribute later on on ` set_prop `
( prop ) => prop . type === 'Property' && /** @type {Identifier} */ ( prop . key ) . name === name
* @ param { string } value
) ;
* /
if ( ! prop ) {
function add _is ( value ) {
prop = b . prop ( 'init' , b . id ( name ) , init ) ;
/** @type {CallExpression} */ ( call . declarations [ 0 ] . init ) . arguments . push (
element . properties . push ( prop ) ;
b . object ( [ b . prop ( 'init' , b . literal ( 'is' ) , b . literal ( value ) ) ] )
) ;
}
}
return {
return /** @type {Property} */ ( prop ) ;
call ,
name ,
element ,
add _is ,
namespaced : namespace != null
} ;
}
}
/ * *
/ * *
* @ param { Scope} scope
* @ param { Element } element
* @ param { string } data
* @ param { string } data
* @ returns { Anchor }
* @ returns { Anchor }
* /
* /
function create _anchor ( scope , data = '' ) {
function create _anchor ( element , data = '' ) {
const name = scope . generate ( 'comment' ) ;
if ( ! element ) return data ? b . array ( [ b . literal ( data ) ] ) : null ;
return {
const c = get _or _create _prop ( element , 'c' , b . array ( [ ] ) ) ;
call : b . var ( name , b . call ( 'document.createComment' , b . literal ( data ) ) ) ,
/** @type {ArrayExpression} */ ( c . value ) . elements . push ( data ? b . array ( [ b . literal ( data ) ] ) : null ) ;
name
} ;
}
}
/ * *
/ * *
* @ param { Scope} scope
* @ param { Element} element
* @ param { string } value
* @ param { string } value
* @ returns { Text }
* @ returns { Text }
* /
* /
function create _text ( scope , value ) {
function create _text ( element , value ) {
const name = scope . generate ( 'text' ) ;
if ( ! element ) return b . literal ( value ) ;
return {
const c = get _or _create _prop ( element , 'c' , b . array ( [ ] ) ) ;
call : b . var ( name , b . call ( 'document.createTextNode' , b . literal ( value ) ) ) ,
/** @type {ArrayExpression} */ ( c . value ) . elements . push ( b . literal ( value ) ) ;
name
} ;
}
}
/ * *
/ * *
*
*
* @ param { Element } el
* @ param { Element } el ement
* @ param { string } prop
* @ param { string } prop
* @ param { string } value
* @ param { string } value
* /
* /
function set _prop ( el , prop , value ) {
function set _prop ( element , prop , value ) {
// see comment above about the "is" attribute
const p = get _or _create _prop ( element , 'p' , b . object ( [ ] ) ) ;
if ( prop === 'is' ) {
if ( prop === 'is' ) {
el . add _is ( value ) ;
el ement. properties . push ( b . prop ( 'init' , b . id ( prop ) , b . literal ( value ) ) ) ;
return ;
return ;
}
}
const [ namespace ] = prop . split ( ':' ) ;
const prop _correct _case = fix _attribute _casing ( prop ) ;
let fn = namespace !== prop ? '.setAttributeNS' : '.setAttribute' ;
let args = [ b . literal ( fix _attribute _casing ( prop ) ) , b . literal ( value ? ? '' ) ] ;
// attributes like `xlink:href` need to be set with the `xlink` namespace
const is _valid _id = regex _is _valid _identifier . test ( prop _correct _case ) ;
if ( namespace === 'xlink' ) {
args . unshift ( b . literal ( 'http://www.w3.org/1999/xlink' ) ) ;
}
return {
/** @type {ObjectExpression} */ ( p . value ) . properties . push (
call : b . call ( el . name + fn , ... args )
b . prop (
} ;
'init' ,
( is _valid _id ? b . id : b . literal ) ( prop _correct _case ) ,
b . literal ( value ) ,
! is _valid _id
)
) ;
}
}
/ * *
/ * *
*
*
* @ param { Element } el
* @ param { Element } element
* @ param { Node } child
* @ param { Element } child
* @ param { Node } [ anchor ]
* /
* /
function insert ( el , child , anchor ) {
function insert ( element , child ) {
return {
const c = get _or _create _prop ( element , 'c' , b . array ( [ ] ) ) ;
call : b . call (
/** @type {ArrayExpression} */ ( c . value ) . elements . push ( child ) ;
// if we have a template element we need to push into it's content rather than the element itself
el . name + ( el . element === 'template' ? '.content' : '' ) + '.insertBefore' ,
b . id ( child . name ) ,
b . id ( anchor ? . name ? ? 'undefined' )
)
} ;
}
}
let map = {
let map = {