svelte
Version:
Cybernetically enhanced web apps
224 lines (188 loc) • 6.46 kB
JavaScript
/** @import { Expression } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
import { is_void } from '../../../../../utils.js';
import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, prepare_element_spread_object } from './shared/element.js';
import {
process_children,
build_template,
create_child_block,
PromiseOptimiser,
create_async_block
} from './shared/utils.js';
/**
* @param {AST.RegularElement} node
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
const namespace = determine_namespace_for_children(node, context.state.namespace);
/** @type {ComponentServerTransformState} */
const state = {
...context.state,
namespace,
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
init: [],
template: []
};
const node_is_void = is_void(node.name);
const optimiser = new PromiseOptimiser();
// If this element needs special handling (like <select value> / <option>),
// avoid calling build_element_attributes here to prevent evaluating/awaiting
// attribute expressions twice. We'll handle attributes in the special branch.
const is_select_special =
node.name === 'select' &&
node.attributes.some(
(attribute) =>
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
);
const is_option_special = node.name === 'option';
const is_special = is_select_special || is_option_special;
let body = /** @type {Expression | null} */ (null);
if (!is_special) {
// only open the tag in the non-special path
state.template.push(b.literal(`<${node.name}`));
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
}
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
state.template.push(
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
);
// TODO this is a real edge case, would be good to DRY this out
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
return;
}
const { hoisted, trimmed } = clean_nodes(
node,
node.fragment.nodes,
context.path,
namespace,
{
...state,
scope: /** @type {Scope} */ (state.scopes.get(node.fragment))
},
state.preserve_whitespace,
state.options.preserveComments
);
for (const node of hoisted) {
context.visit(node, state);
}
if (dev) {
const location = locator(node.start);
state.template.push(
b.stmt(
b.call(
'$.push_element',
b.id('$$renderer'),
b.literal(node.name),
b.literal(location.line),
b.literal(location.column)
)
)
);
}
if (is_select_special) {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
const fn = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (is_option_special) {
let body;
if (node.metadata.synthetic_value_node) {
body = optimiser.transform(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.expression
);
} else {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
body = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
}
const [attributes, ...rest] = prepare_element_spread_object(node, context, optimiser.transform);
const statement = b.stmt(b.call('$$renderer.option', attributes, body, ...rest));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
let id = /** @type {Expression} */ (body);
if (body.type !== 'Identifier') {
id = b.id(state.scope.generate('$$body'));
state.template.push(b.const(id, body));
}
// Use the body expression as the body if it's truthy, otherwise use the inner template
state.template.push(
b.if(
id,
b.block(build_template([id])),
b.block([...inner_state.init, ...build_template(inner_state.template)])
)
);
} else {
process_children(trimmed, { ...context, state });
}
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
}
if (dev) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.is_async()) {
let statement = create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
);
const blockers = optimiser.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(b.block([statement]), blockers, false, false);
}
context.state.template.push(statement);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
}