alternative attachment syntax

attachments-2
Rich Harris 8 months ago
parent 6402161932
commit f2d4792406

@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {

@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {

@ -1,4 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { CallExpression, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
@ -14,6 +14,8 @@ import { get_attribute_expression, is_expression_attribute } from '../../../util
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.js';
import { find_matching_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -480,31 +482,37 @@ function read_static_attribute(parser) {
/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment | null}
*/
function read_attribute(parser) {
const start = parser.index;
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.match('attach(')) {
const end = find_matching_bracket(parser.template, start + 7, '(');
if (parser.eat('@attach')) {
parser.require_whitespace();
if (end === undefined) {
e.unexpected_eof(parser.template.length);
}
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
const sliced = parser.template.slice(0, end + 1);
/** @type {AST.AttachTag} */
const attachment = {
type: 'AttachTag',
start,
end: parser.index,
expression
};
const call = /** @type {CallExpression} */ (parse_expression_at(sliced, parser.ts, start));
return attachment;
}
/** @type {AST.Attachment} */
const attachment = {
type: 'Attachment',
start,
end,
attachments: call.arguments
};
parser.index = end + 1;
return attachment;
}
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('...')) {
const expression = read_expression(parser);

@ -76,7 +76,7 @@ export function visit_component(node, context) {
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective' &&
attribute.type !== 'AttachTag'
attribute.type !== 'Attachment'
) {
e.component_invalid_directive(attribute);
}
@ -110,10 +110,6 @@ export function visit_component(node, context) {
if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
context.state.analysis.uses_component_bindings = true;
}
if (attribute.type === 'AttachTag') {
disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source);
}
}
// If the component has a slot attribute — `<Foo slot="whatever" .../>` —

@ -56,7 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { AttachTag } from './visitors/AttachTag.js';
import { Attachment } from './visitors/Attachment.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
@ -132,7 +132,7 @@ const visitors = {
TransitionDirective,
UpdateExpression,
UseDirective,
AttachTag,
Attachment,
VariableDeclaration
};

@ -1,21 +0,0 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
)
)
);
context.next();
}

@ -0,0 +1,34 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AST.Attachment} node
* @param {ComponentContext} context
*/
export function Attachment(node, context) {
for (const attachment of node.attachments) {
if (attachment.type === 'SpreadElement') {
context.state.init.push(
b.stmt(
b.call(
'$.attach_all',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(attachment.argument)))
)
)
);
} else {
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(attachment)))
)
)
);
}
}
}

@ -82,7 +82,7 @@ export function RegularElement(node, context) {
/** @type {AST.StyleDirective[]} */
const style_directives = [];
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.AttachTag>} */
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.Attachment>} */
const other_directives = [];
/** @type {ExpressionStatement[]} */
@ -153,7 +153,7 @@ export function RegularElement(node, context) {
other_directives.push(attribute);
break;
case 'AttachTag':
case 'Attachment':
other_directives.push(attribute);
break;
}

@ -261,16 +261,20 @@ export function build_component(node, component_name, context, anchor = context.
);
}
}
} else if (attribute.type === 'AttachTag') {
} else if (attribute.type === 'Attachment') {
// TODO do we need to create a derived here?
push_prop(
b.prop(
'get',
b.call('Symbol'),
/** @type {Expression} */ (context.visit(attribute.expression)),
true
)
);
for (const attachment of attribute.attachments) {
push_prop(
b.prop(
'get',
b.call('Symbol'),
/** @type {Expression} */ (
context.visit(attachment.type === 'SpreadElement' ? attachment.argument : attachment)
),
true
)
);
}
}
}

@ -15,7 +15,8 @@ import type {
Program,
ChainExpression,
SimpleCallExpression,
SequenceExpression
SequenceExpression,
SpreadElement
} from 'estree';
import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
@ -174,10 +175,10 @@ export namespace AST {
};
}
/** A `{@attach foo(...)} tag */
export interface AttachTag extends BaseNode {
type: 'AttachTag';
expression: Expression;
/** An `attach(...)` attribute */
export interface Attachment extends BaseNode {
type: 'Attachment';
attachments: Array<Expression | SpreadElement>;
}
/** An `animate:` directive */
@ -279,7 +280,7 @@ export namespace AST {
interface BaseElement extends BaseNode {
name: string;
attributes: Array<Attribute | SpreadAttribute | Directive | AttachTag>;
attributes: Array<Attribute | SpreadAttribute | Directive | Attachment>;
fragment: Fragment;
}
@ -555,7 +556,7 @@ export namespace AST {
| AST.Attribute
| AST.SpreadAttribute
| Directive
| AST.AttachTag
| AST.Attachment
| AST.Comment
| Block;

@ -6,10 +6,16 @@ import { effect } from '../../reactivity/effects.js';
*/
export function attach(node, get_fn) {
effect(() => {
const fn = get_fn();
const attachment = get_fn();
// we use `&&` rather than `?.` so that things like
// `{@attach DEV && something_dev_only()}` work
return fn && fn(node);
if (Array.isArray(attachment)) {
for (const fn of attachment) {
if (fn) {
$effect(() => fn(node));
}
}
} else if (attachment) {
return attachment(node);
}
});
}

@ -1 +1,5 @@
<div {@attach (node) => {}} {@attach (node) => {}}></div>
<div attach(a)></div>
<div attach(a, b, c)></div>
<div attach(...stuff)></div>
<div attach(a, b, c, ...stuff)></div>
<div attach((node) => {})></div>

@ -2,7 +2,7 @@
"css": null,
"js": [],
"start": 0,
"end": 57,
"end": 149,
"type": "Root",
"fragment": {
"type": "Fragment",
@ -10,124 +10,381 @@
{
"type": "RegularElement",
"start": 0,
"end": 57,
"end": 21,
"name": "div",
"attributes": [
{
"type": "AttachTag",
"start": 5,
"end": 27,
"expression": {
"type": "ArrowFunctionExpression",
"start": 14,
"end": 26,
"loc": {
"start": {
"line": 1,
"column": 14
"end": 13,
"attachments": [
{
"type": "Identifier",
"start": 12,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 12
},
"end": {
"line": 1,
"column": 13
}
},
"end": {
"line": 1,
"column": 26
}
"name": "a"
}
]
},
{
"type": "Attribute",
"start": 13,
"end": 14,
"name": ")",
"value": true
}
],
"fragment": {
"type": "Fragment",
"nodes": []
}
},
{
"type": "Text",
"start": 21,
"end": 22,
"raw": "\n",
"data": "\n"
},
{
"type": "RegularElement",
"start": 22,
"end": 49,
"name": "div",
"attributes": [
{
"type": "AttachTag",
"start": 27,
"end": 41,
"attachments": [
{
"type": "Identifier",
"start": 34,
"end": 35,
"loc": {
"start": {
"line": 2,
"column": 12
},
"end": {
"line": 2,
"column": 13
}
},
"name": "a"
},
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
{
"type": "Identifier",
"start": 37,
"end": 38,
"loc": {
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 16
}
},
"name": "b"
},
{
"type": "Identifier",
"start": 40,
"end": 41,
"loc": {
"start": {
"line": 2,
"column": 18
},
"end": {
"line": 2,
"column": 19
}
},
"name": "c"
}
]
},
{
"type": "Attribute",
"start": 41,
"end": 42,
"name": ")",
"value": true
}
],
"fragment": {
"type": "Fragment",
"nodes": []
}
},
{
"type": "Text",
"start": 49,
"end": 50,
"raw": "\n",
"data": "\n"
},
{
"type": "RegularElement",
"start": 50,
"end": 78,
"name": "div",
"attributes": [
{
"type": "AttachTag",
"start": 55,
"end": 70,
"attachments": [
{
"type": "SpreadElement",
"start": 62,
"end": 70,
"loc": {
"start": {
"line": 3,
"column": 12
},
"end": {
"line": 3,
"column": 20
}
},
"argument": {
"type": "Identifier",
"start": 15,
"end": 19,
"start": 65,
"end": 70,
"loc": {
"start": {
"line": 1,
"line": 3,
"column": 15
},
"end": {
"line": 1,
"column": 19
"line": 3,
"column": 20
}
},
"name": "node"
"name": "stuff"
}
],
"body": {
"type": "BlockStatement",
"start": 24,
"end": 26,
}
]
},
{
"type": "Attribute",
"start": 70,
"end": 71,
"name": ")",
"value": true
}
],
"fragment": {
"type": "Fragment",
"nodes": []
}
},
{
"type": "Text",
"start": 78,
"end": 79,
"raw": "\n",
"data": "\n"
},
{
"type": "RegularElement",
"start": 79,
"end": 116,
"name": "div",
"attributes": [
{
"type": "AttachTag",
"start": 84,
"end": 108,
"attachments": [
{
"type": "Identifier",
"start": 91,
"end": 92,
"loc": {
"start": {
"line": 1,
"column": 24
"line": 4,
"column": 12
},
"end": {
"line": 1,
"column": 26
"line": 4,
"column": 13
}
},
"body": []
}
}
},
{
"type": "AttachTag",
"start": 28,
"end": 50,
"expression": {
"type": "ArrowFunctionExpression",
"start": 37,
"end": 49,
"loc": {
"start": {
"line": 1,
"column": 37
"name": "a"
},
{
"type": "Identifier",
"start": 94,
"end": 95,
"loc": {
"start": {
"line": 4,
"column": 15
},
"end": {
"line": 4,
"column": 16
}
},
"end": {
"line": 1,
"column": 49
}
"name": "b"
},
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
{
"type": "Identifier",
"start": 97,
"end": 98,
"loc": {
"start": {
"line": 4,
"column": 18
},
"end": {
"line": 4,
"column": 19
}
},
"name": "c"
},
{
"type": "SpreadElement",
"start": 100,
"end": 108,
"loc": {
"start": {
"line": 4,
"column": 21
},
"end": {
"line": 4,
"column": 29
}
},
"argument": {
"type": "Identifier",
"start": 38,
"end": 42,
"start": 103,
"end": 108,
"loc": {
"start": {
"line": 1,
"column": 38
"line": 4,
"column": 24
},
"end": {
"line": 1,
"column": 42
"line": 4,
"column": 29
}
},
"name": "node"
"name": "stuff"
}
],
"body": {
"type": "BlockStatement",
"start": 47,
"end": 49,
}
]
},
{
"type": "Attribute",
"start": 108,
"end": 109,
"name": ")",
"value": true
}
],
"fragment": {
"type": "Fragment",
"nodes": []
}
},
{
"type": "Text",
"start": 116,
"end": 117,
"raw": "\n",
"data": "\n"
},
{
"type": "RegularElement",
"start": 117,
"end": 149,
"name": "div",
"attributes": [
{
"type": "AttachTag",
"start": 122,
"end": 141,
"attachments": [
{
"type": "ArrowFunctionExpression",
"start": 129,
"end": 141,
"loc": {
"start": {
"line": 1,
"column": 47
"line": 5,
"column": 12
},
"end": {
"line": 1,
"column": 49
"line": 5,
"column": 24
}
},
"body": []
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 130,
"end": 134,
"loc": {
"start": {
"line": 5,
"column": 13
},
"end": {
"line": 5,
"column": 17
}
},
"name": "node"
}
],
"body": {
"type": "BlockStatement",
"start": 139,
"end": 141,
"loc": {
"start": {
"line": 5,
"column": 22
},
"end": {
"line": 5,
"column": 24
}
},
"body": []
}
}
}
]
},
{
"type": "Attribute",
"start": 141,
"end": 142,
"name": ")",
"value": true
}
],
"fragment": {

@ -1 +1 @@
<div {@attach (node) => node.textContent = node.nodeName}></div>
<div attach((node) => node.textContent = node.nodeName)></div>

@ -2,4 +2,4 @@
import Child from './Child.svelte';
</script>
<Child {@attach (node) => node.textContent = 'set from component'} />
<Child attach((node) => node.textContent = 'set from component') />

@ -2,5 +2,5 @@
let value = $state(1);
</script>
<div {@attach (node) => node.textContent = value}></div>
<div attach((node) => node.textContent = value)></div>
<button onclick={() => value += 1}>increment</button>

@ -1 +1 @@
<svelte:element this={'div'} {@attach (node) => node.textContent = node.nodeName}></svelte:element>
<svelte:element this={'div'} attach((node) => node.textContent = node.nodeName)></svelte:element>

@ -622,7 +622,7 @@ declare module 'svelte/animate' {
}
declare module 'svelte/compiler' {
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SpreadElement } from 'estree';
import type { SourceMap } from 'magic-string';
import type { Location } from 'locate-character';
/**
@ -1053,7 +1053,7 @@ declare module 'svelte/compiler' {
/** A `{@attach foo(...)} tag */
export interface AttachTag extends BaseNode {
type: 'AttachTag';
expression: Expression;
attachments: Array<Expression | SpreadElement>;
}
/** An `animate:` directive */

Loading…
Cancel
Save