|
|
|
@ -1,131 +1,133 @@
|
|
|
|
import Node from './shared/Node';
|
|
|
|
import Node from './shared/Node.js';
|
|
|
|
import get_object from '../utils/get_object';
|
|
|
|
import get_object from '../utils/get_object.js';
|
|
|
|
import Expression from './shared/Expression';
|
|
|
|
import Expression from './shared/Expression.js';
|
|
|
|
import Component from '../Component';
|
|
|
|
import { regex_dimensions, regex_box_size } from '../../utils/patterns.js';
|
|
|
|
import TemplateScope from './shared/TemplateScope';
|
|
|
|
import { clone } from '../../utils/clone.js';
|
|
|
|
import { regex_dimensions, regex_box_size } from '../../utils/patterns';
|
|
|
|
import compiler_errors from '../compiler_errors.js';
|
|
|
|
import { Node as ESTreeNode } from 'estree';
|
|
|
|
import compiler_warnings from '../compiler_warnings.js';
|
|
|
|
import { TemplateNode } from '../../interfaces';
|
|
|
|
|
|
|
|
import Element from './Element';
|
|
|
|
|
|
|
|
import InlineComponent from './InlineComponent';
|
|
|
|
|
|
|
|
import Window from './Window';
|
|
|
|
|
|
|
|
import Document from './Document';
|
|
|
|
|
|
|
|
import { clone } from '../../utils/clone';
|
|
|
|
|
|
|
|
import compiler_errors from '../compiler_errors';
|
|
|
|
|
|
|
|
import compiler_warnings from '../compiler_warnings';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO this should live in a specific binding
|
|
|
|
// TODO this should live in a specific binding
|
|
|
|
const read_only_media_attributes = new Set([
|
|
|
|
const read_only_media_attributes = new Set([
|
|
|
|
'duration',
|
|
|
|
'duration',
|
|
|
|
'buffered',
|
|
|
|
'buffered',
|
|
|
|
'seekable',
|
|
|
|
'seekable',
|
|
|
|
'played',
|
|
|
|
'played',
|
|
|
|
'seeking',
|
|
|
|
'seeking',
|
|
|
|
'ended',
|
|
|
|
'ended',
|
|
|
|
'videoHeight',
|
|
|
|
'videoHeight',
|
|
|
|
'videoWidth',
|
|
|
|
'videoWidth',
|
|
|
|
'naturalWidth',
|
|
|
|
'naturalWidth',
|
|
|
|
'naturalHeight',
|
|
|
|
'naturalHeight',
|
|
|
|
'readyState'
|
|
|
|
'readyState'
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @extends Node */
|
|
|
|
export default class Binding extends Node {
|
|
|
|
export default class Binding extends Node {
|
|
|
|
type: 'Binding';
|
|
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
|
|
expression: Expression;
|
|
|
|
|
|
|
|
raw_expression: ESTreeNode; // TODO exists only for bind:this — is there a more elegant solution?
|
|
|
|
|
|
|
|
is_contextual: boolean;
|
|
|
|
|
|
|
|
is_readonly: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
|
|
component: Component,
|
|
|
|
|
|
|
|
parent: Element | InlineComponent | Window | Document,
|
|
|
|
|
|
|
|
scope: TemplateScope,
|
|
|
|
|
|
|
|
info: TemplateNode
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
super(component, parent, scope, info);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
|
|
|
|
|
|
|
|
component.error(info, compiler_errors.invalid_directive_value);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.name = info.name;
|
|
|
|
|
|
|
|
this.expression = new Expression(component, this, scope, info.expression);
|
|
|
|
|
|
|
|
this.raw_expression = clone(info.expression);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { name } = get_object(this.expression.node);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.is_contextual = Array.from(this.expression.references).some((name) =>
|
|
|
|
|
|
|
|
scope.names.has(name)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
if (this.is_contextual) this.validate_binding_rest_properties(scope);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// make sure we track this as a mutable ref
|
|
|
|
|
|
|
|
if (scope.is_let(name)) {
|
|
|
|
|
|
|
|
component.error(this, compiler_errors.invalid_binding_let);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
} else if (scope.names.has(name)) {
|
|
|
|
|
|
|
|
if (scope.is_await(name)) {
|
|
|
|
|
|
|
|
component.error(this, compiler_errors.invalid_binding_await);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scope.is_const(name)) {
|
|
|
|
|
|
|
|
component.error(this, compiler_errors.invalid_binding_const);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scope.dependencies_for_name.get(name).forEach((name) => {
|
|
|
|
/** @type {'Binding'} */
|
|
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
type;
|
|
|
|
if (variable) {
|
|
|
|
|
|
|
|
variable.mutated = true;
|
|
|
|
/** @type {string} */
|
|
|
|
}
|
|
|
|
name;
|
|
|
|
});
|
|
|
|
|
|
|
|
} else {
|
|
|
|
/** @type {import('./shared/Expression.js').default} */
|
|
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
expression;
|
|
|
|
|
|
|
|
|
|
|
|
if (!variable || variable.global) {
|
|
|
|
/** @type {ESTreeNode} */
|
|
|
|
component.error(this.expression.node as any, compiler_errors.binding_undeclared(name));
|
|
|
|
raw_expression; // TODO exists only for bind:this — is there a more elegant solution?
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
/** @type {boolean} */
|
|
|
|
|
|
|
|
is_contextual;
|
|
|
|
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {boolean} */
|
|
|
|
if (info.expression.type === 'Identifier' && !variable.writable) {
|
|
|
|
is_readonly;
|
|
|
|
component.error(this.expression.node as any, compiler_errors.invalid_binding_writable);
|
|
|
|
|
|
|
|
return;
|
|
|
|
/**
|
|
|
|
}
|
|
|
|
* @param {import('../Component.js').default} component *
|
|
|
|
}
|
|
|
|
* @param {import('./Element.js').default | import('./InlineComponent.js').default | import('./Window.js').default | import('./Document.js').default} parent *
|
|
|
|
|
|
|
|
* @param {import('./shared/TemplateScope.js').default} scope *
|
|
|
|
|
|
|
|
* @param {import('../../interfaces.js').TemplateNode} info undefined
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
constructor(component, parent, scope, info) {
|
|
|
|
|
|
|
|
super(component, parent, scope, info);
|
|
|
|
|
|
|
|
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
|
|
|
|
|
|
|
|
component.error(info, compiler_errors.invalid_directive_value);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.name = info.name;
|
|
|
|
|
|
|
|
this.expression = new Expression(component, this, scope, info.expression);
|
|
|
|
|
|
|
|
this.raw_expression = clone(info.expression);
|
|
|
|
|
|
|
|
const { name } = get_object(this.expression.node);
|
|
|
|
|
|
|
|
this.is_contextual = Array.from(this.expression.references).some(/** @param {any} name */ (name) => scope.names.has(name));
|
|
|
|
|
|
|
|
if (this.is_contextual)
|
|
|
|
|
|
|
|
this.validate_binding_rest_properties(scope);
|
|
|
|
|
|
|
|
// make sure we track this as a mutable ref
|
|
|
|
|
|
|
|
if (scope.is_let(name)) {
|
|
|
|
|
|
|
|
component.error(this, compiler_errors.invalid_binding_let);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else if (scope.names.has(name)) {
|
|
|
|
|
|
|
|
if (scope.is_await(name)) {
|
|
|
|
|
|
|
|
component.error(this, compiler_errors.invalid_binding_await);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scope.is_const(name)) {
|
|
|
|
|
|
|
|
component.error(this, compiler_errors.invalid_binding_const);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
scope.dependencies_for_name.get(name).forEach(/** @param {any} name */ (name) => {
|
|
|
|
|
|
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
|
|
|
|
if (variable) {
|
|
|
|
|
|
|
|
variable.mutated = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
|
|
|
|
if (!variable || variable.global) {
|
|
|
|
|
|
|
|
component.error(/** @type {any} */ (this.expression.node), compiler_errors.binding_undeclared(name));
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
|
|
|
|
|
|
|
|
if (info.expression.type === 'Identifier' && !variable.writable) {
|
|
|
|
|
|
|
|
component.error(/** @type {any} */ (this.expression.node), compiler_errors.invalid_binding_writable);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const type = parent.get_static_attribute_value('type');
|
|
|
|
|
|
|
|
this.is_readonly =
|
|
|
|
|
|
|
|
regex_dimensions.test(this.name) ||
|
|
|
|
|
|
|
|
regex_box_size.test(this.name) ||
|
|
|
|
|
|
|
|
(isElement(parent) &&
|
|
|
|
|
|
|
|
((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
|
|
|
|
|
|
|
|
(parent.name === 'input' && type === 'file'))) /* TODO others? */;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
is_readonly_media_attribute() {
|
|
|
|
|
|
|
|
return read_only_media_attributes.has(this.name);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @param {import('./shared/TemplateScope.js').default} scope */
|
|
|
|
|
|
|
|
validate_binding_rest_properties(scope) {
|
|
|
|
|
|
|
|
this.expression.references.forEach(/** @param {any} name */ (name) => {
|
|
|
|
|
|
|
|
const each_block = scope.get_owner(name);
|
|
|
|
|
|
|
|
if (each_block && each_block.type === 'EachBlock') {
|
|
|
|
|
|
|
|
const rest_node = each_block.context_rest_properties.get(name);
|
|
|
|
|
|
|
|
if (rest_node) {
|
|
|
|
|
|
|
|
this.component.warn(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {any} */ (rest_node), compiler_warnings.invalid_rest_eachblock_binding(name));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const type = parent.get_static_attribute_value('type');
|
|
|
|
/**
|
|
|
|
|
|
|
|
* @param {import('./shared/Node.js').default} node
|
|
|
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
function isElement(node) {
|
|
|
|
|
|
|
|
return !!( /** @type {any} */(node)).is_media_node;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.is_readonly =
|
|
|
|
|
|
|
|
regex_dimensions.test(this.name) ||
|
|
|
|
|
|
|
|
regex_box_size.test(this.name) ||
|
|
|
|
|
|
|
|
(isElement(parent) &&
|
|
|
|
|
|
|
|
((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
|
|
|
|
|
|
|
|
(parent.name === 'input' && type === 'file'))) /* TODO others? */;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
is_readonly_media_attribute() {
|
|
|
|
|
|
|
|
return read_only_media_attributes.has(this.name);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validate_binding_rest_properties(scope: TemplateScope) {
|
|
|
|
|
|
|
|
this.expression.references.forEach((name) => {
|
|
|
|
|
|
|
|
const each_block = scope.get_owner(name);
|
|
|
|
|
|
|
|
if (each_block && each_block.type === 'EachBlock') {
|
|
|
|
|
|
|
|
const rest_node = each_block.context_rest_properties.get(name);
|
|
|
|
|
|
|
|
if (rest_node) {
|
|
|
|
|
|
|
|
this.component.warn(
|
|
|
|
|
|
|
|
rest_node as any,
|
|
|
|
|
|
|
|
compiler_warnings.invalid_rest_eachblock_binding(name)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isElement(node: Node): node is Element {
|
|
|
|
|
|
|
|
return !!(node as any).is_media_node;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|