UNPKG

svelte

Version:

Cybernetically enhanced web apps

446 lines (399 loc) • 13.5 kB
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ /** @import { AST, Namespace } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { get_attribute_chunks, is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { binding_properties } from '../../../../bindings.js'; import { create_attribute, create_expression_metadata, is_custom_element_node } from '../../../../nodes.js'; import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '../../../../../utils/builders.js'; import { ELEMENT_IS_NAMESPACED, ELEMENT_PRESERVE_ATTRIBUTE_CASE } from '../../../../../../constants.js'; import { build_attribute_value } from './utils.js'; import { is_boolean_attribute, is_content_editable_binding, is_load_error_element } from '../../../../../../utils.js'; import { escape_html } from '../../../../../../escaping.js'; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; /** * Writes the output to the template output. Some elements may have attributes on them that require the * their output to be the child content instead. In this case, an object is returned. * @param {AST.RegularElement | AST.SvelteElement} node * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context */ export function build_element_attributes(node, context) { /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */ const attributes = []; /** @type {AST.ClassDirective[]} */ const class_directives = []; /** @type {AST.StyleDirective[]} */ const style_directives = []; /** @type {Expression | null} */ let content = null; let has_spread = false; let events_to_capture = new Set(); for (const attribute of node.attributes) { if (attribute.type === 'Attribute') { if (attribute.name === 'value') { if (node.name === 'textarea') { if ( attribute.value !== true && Array.isArray(attribute.value) && attribute.value[0].type === 'Text' && regex_starts_with_newline.test(attribute.value[0].data) ) { // Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`. // see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions // also see related code in analysis phase attribute.value[0].data = '\n' + attribute.value[0].data; } content = b.call('$.escape', build_attribute_value(attribute.value, context)); } else if (node.name !== 'select') { // omit value attribute for select elements, it's irrelevant for the initially selected value and has no // effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute) attributes.push(attribute); } // omit event handlers except for special cases } else if (is_event_attribute(attribute)) { if ( (attribute.name === 'onload' || attribute.name === 'onerror') && is_load_error_element(node.name) ) { events_to_capture.add(attribute.name); } // the defaultValue/defaultChecked properties don't exist as attributes } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { if (attribute.name === 'class') { if (attribute.metadata.needs_clsx) { attributes.push({ ...attribute, value: { .../** @type {AST.ExpressionTag} */ (attribute.value), expression: b.call( '$.clsx', /** @type {AST.ExpressionTag} */ (attribute.value).expression ) } }); } else { attributes.push(attribute); } } else { attributes.push(attribute); } } } else if (attribute.type === 'BindDirective') { if (attribute.name === 'value' && node.name === 'select') continue; if ( attribute.name === 'value' && attributes.some( (attr) => attr.type === 'Attribute' && attr.name === 'type' && is_text_attribute(attr) && attr.value[0].data === 'file' ) ) { continue; } if (attribute.name === 'this') continue; const binding = binding_properties[attribute.name]; if (binding?.omit_in_ssr) continue; let expression = /** @type {Expression} */ (context.visit(attribute.expression)); if (expression.type === 'SequenceExpression') { expression = b.call(expression.expressions[0]); } if (is_content_editable_binding(attribute.name)) { content = expression; } else if (attribute.name === 'value' && node.name === 'textarea') { content = b.call('$.escape', expression); } else if (attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression') { const value_attribute = /** @type {AST.Attribute | undefined} */ ( node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') ); if (!value_attribute) continue; const is_checkbox = node.attributes.some( (attr) => attr.type === 'Attribute' && attr.name === 'type' && is_text_attribute(attr) && attr.value[0].data === 'checkbox' ); attributes.push( create_attribute('checked', -1, -1, [ { type: 'ExpressionTag', start: -1, end: -1, expression: is_checkbox ? b.call( b.member(attribute.expression, 'includes'), build_attribute_value(value_attribute.value, context) ) : b.binary( '===', attribute.expression, build_attribute_value(value_attribute.value, context) ), metadata: { expression: create_expression_metadata() } } ]) ); } else { attributes.push( create_attribute(attribute.name, -1, -1, [ { type: 'ExpressionTag', start: -1, end: -1, expression, metadata: { expression: create_expression_metadata() } } ]) ); } } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); has_spread = true; if (is_load_error_element(node.name)) { events_to_capture.add('onload'); events_to_capture.add('onerror'); } } else if (attribute.type === 'UseDirective') { if (is_load_error_element(node.name)) { events_to_capture.add('onload'); events_to_capture.add('onerror'); } } else if (attribute.type === 'ClassDirective') { class_directives.push(attribute); } else if (attribute.type === 'StyleDirective') { style_directives.push(attribute); } else if (attribute.type === 'LetDirective') { // do nothing, these are handled inside `build_inline_component` } else { context.visit(attribute); } } if (has_spread) { build_element_spread_attributes(node, attributes, style_directives, class_directives, context); } else { const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { const name = get_attribute_name(node, attribute); const can_use_literal = (name !== 'class' || class_directives.length === 0) && (name !== 'style' || style_directives.length === 0); if (can_use_literal && (attribute.value === true || is_text_attribute(attribute))) { let literal_value = /** @type {Literal} */ ( build_attribute_value( attribute.value, context, WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ) ).value; if (name === 'class' && css_hash) { literal_value = (String(literal_value) + ' ' + css_hash).trim(); } if (name !== 'class' || literal_value) { context.state.template.push( b.literal( ` ${attribute.name}${ is_boolean_attribute(name) && literal_value === true ? '' : `="${literal_value === true ? '' : String(literal_value)}"` }` ) ); } continue; } const value = build_attribute_value( attribute.value, context, WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ); // pre-escape and inline literal attributes : if (can_use_literal && value.type === 'Literal' && typeof value.value === 'string') { if (name === 'class' && css_hash) { value.value = (value.value + ' ' + css_hash).trim(); } context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`)); } else if (name === 'class') { context.state.template.push(build_attr_class(class_directives, value, context, css_hash)); } else if (name === 'style') { context.state.template.push(build_attr_style(style_directives, value, context)); } else { context.state.template.push( b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) ); } } } if (events_to_capture.size !== 0) { for (const event of events_to_capture) { context.state.template.push(b.literal(` ${event}="this.__e=event"`)); } } return content; } /** * @param {AST.RegularElement | AST.SvelteElement} element * @param {AST.Attribute} attribute */ function get_attribute_name(element, attribute) { let name = attribute.name; if (!element.metadata.svg && !element.metadata.mathml) { name = name.toLowerCase(); // don't lookup boolean aliases here, the server runtime function does only // check for the lowercase variants of boolean attributes } return name; } /** * * @param {AST.RegularElement | AST.SvelteElement} element * @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes * @param {AST.StyleDirective[]} style_directives * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context */ function build_element_spread_attributes( element, attributes, style_directives, class_directives, context ) { let classes; let styles; let flags = 0; if (class_directives.length) { const properties = class_directives.map((directive) => b.init( directive.name, directive.expression.type === 'Identifier' && directive.expression.name === directive.name ? b.id(directive.name) : /** @type {Expression} */ (context.visit(directive.expression)) ) ); classes = b.object(properties); } if (style_directives.length > 0) { const properties = style_directives.map((directive) => b.init( directive.name, directive.value === true ? b.id(directive.name) : build_attribute_value(directive.value, context, true) ) ); styles = b.object(properties); } if (element.metadata.svg || element.metadata.mathml) { flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; } else if (is_custom_element_node(element)) { flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; } const object = b.object( attributes.map((attribute) => { if (attribute.type === 'Attribute') { const name = get_attribute_name(element, attribute); const value = build_attribute_value( attribute.value, context, WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ); return b.prop('init', b.key(name), value); } return b.spread(/** @type {Expression} */ (context.visit(attribute))); }) ); const css_hash = element.metadata.scoped && context.state.analysis.css.hash ? b.literal(context.state.analysis.css.hash) : b.null; const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined]; context.state.template.push(b.call('$.spread_attributes', ...args)); } /** * * @param {AST.ClassDirective[]} class_directives * @param {Expression} expression * @param {ComponentContext} context * @param {string | null} hash */ function build_attr_class(class_directives, expression, context, hash) { /** @type {ObjectExpression | undefined} */ let directives; if (class_directives.length) { directives = b.object( class_directives.map((directive) => b.prop( 'init', b.literal(directive.name), /** @type {Expression} */ (context.visit(directive.expression, context.state)) ) ) ); } let css_hash; if (hash) { if (expression.type === 'Literal' && typeof expression.value === 'string') { expression.value = (expression.value + ' ' + hash).trim(); } else { css_hash = b.literal(hash); } } return b.call('$.attr_class', expression, css_hash, directives); } /** * * @param {AST.StyleDirective[]} style_directives * @param {Expression} expression * @param {ComponentContext} context */ function build_attr_style(style_directives, expression, context) { /** @type {ArrayExpression | ObjectExpression | undefined} */ let directives; if (style_directives.length) { let normal_properties = []; let important_properties = []; for (const directive of style_directives) { const expression = directive.value === true ? b.id(directive.name) : build_attribute_value(directive.value, context, true); let name = directive.name; if (name[0] !== '-' || name[1] !== '-') { name = name.toLowerCase(); } const property = b.init(directive.name, expression); if (directive.modifiers.includes('important')) { important_properties.push(property); } else { normal_properties.push(property); } } if (important_properties.length) { directives = b.array([b.object(normal_properties), b.object(important_properties)]); } else { directives = b.object(normal_properties); } } return b.call('$.attr_style', expression, directives); }