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 {Parser} parser
* @param {number} start * @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} * @returns {AST.Script}
*/ */
export function read_script(parser, start, attributes) { export function read_script(parser, start, attributes) {

@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {number} start * @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} * @returns {AST.CSS.StyleSheet}
*/ */
export default function read_style(parser, start, attributes) { 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 { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.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 { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js'; import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.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_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -480,32 +482,38 @@ function read_static_attribute(parser) {
/** /**
* @param {Parser} 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) { function read_attribute(parser) {
const start = parser.index; const start = parser.index;
if (parser.eat('{')) { if (parser.match('attach(')) {
parser.allow_whitespace(); const end = find_matching_bracket(parser.template, start + 7, '(');
if (parser.eat('@attach')) { if (end === undefined) {
parser.require_whitespace(); e.unexpected_eof(parser.template.length);
}
const expression = read_expression(parser); const sliced = parser.template.slice(0, end + 1);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.AttachTag} */ const call = /** @type {CallExpression} */ (parse_expression_at(sliced, parser.ts, start));
/** @type {AST.Attachment} */
const attachment = { const attachment = {
type: 'AttachTag', type: 'Attachment',
start, start,
end: parser.index, end,
expression attachments: call.arguments
}; };
parser.index = end + 1;
return attachment; return attachment;
} }
if (parser.eat('{')) {
parser.allow_whitespace();
if (parser.eat('...')) { if (parser.eat('...')) {
const expression = read_expression(parser); const expression = read_expression(parser);

@ -76,7 +76,7 @@ export function visit_component(node, context) {
attribute.type !== 'LetDirective' && attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' && attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective' && attribute.type !== 'BindDirective' &&
attribute.type !== 'AttachTag' attribute.type !== 'Attachment'
) { ) {
e.component_invalid_directive(attribute); e.component_invalid_directive(attribute);
} }
@ -110,10 +110,6 @@ export function visit_component(node, context) {
if (attribute.type === 'BindDirective' && attribute.name !== 'this') { if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
context.state.analysis.uses_component_bindings = true; 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" .../>` — // 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 { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.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'; import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */ /** @type {Visitors} */
@ -132,7 +132,7 @@ const visitors = {
TransitionDirective, TransitionDirective,
UpdateExpression, UpdateExpression,
UseDirective, UseDirective,
AttachTag, Attachment,
VariableDeclaration 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[]} */ /** @type {AST.StyleDirective[]} */
const style_directives = []; 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 = []; const other_directives = [];
/** @type {ExpressionStatement[]} */ /** @type {ExpressionStatement[]} */
@ -153,7 +153,7 @@ export function RegularElement(node, context) {
other_directives.push(attribute); other_directives.push(attribute);
break; break;
case 'AttachTag': case 'Attachment':
other_directives.push(attribute); other_directives.push(attribute);
break; break;
} }

@ -261,18 +261,22 @@ 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? // TODO do we need to create a derived here?
for (const attachment of attribute.attachments) {
push_prop( push_prop(
b.prop( b.prop(
'get', 'get',
b.call('Symbol'), b.call('Symbol'),
/** @type {Expression} */ (context.visit(attribute.expression)), /** @type {Expression} */ (
context.visit(attachment.type === 'SpreadElement' ? attachment.argument : attachment)
),
true true
) )
); );
} }
} }
}
delayed_props.forEach((fn) => fn()); delayed_props.forEach((fn) => fn());

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

@ -6,10 +6,16 @@ import { effect } from '../../reactivity/effects.js';
*/ */
export function attach(node, get_fn) { export function attach(node, get_fn) {
effect(() => { effect(() => {
const fn = get_fn(); const attachment = get_fn();
// we use `&&` rather than `?.` so that things like if (Array.isArray(attachment)) {
// `{@attach DEV && something_dev_only()}` work for (const fn of attachment) {
return fn && fn(node); 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, "css": null,
"js": [], "js": [],
"start": 0, "start": 0,
"end": 57, "end": 149,
"type": "Root", "type": "Root",
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
@ -10,83 +10,332 @@
{ {
"type": "RegularElement", "type": "RegularElement",
"start": 0, "start": 0,
"end": 57, "end": 21,
"name": "div", "name": "div",
"attributes": [ "attributes": [
{ {
"type": "AttachTag", "type": "AttachTag",
"start": 5, "start": 5,
"end": 27, "end": 13,
"expression": { "attachments": [
"type": "ArrowFunctionExpression", {
"start": 14, "type": "Identifier",
"end": 26, "start": 12,
"end": 13,
"loc": { "loc": {
"start": { "start": {
"line": 1, "line": 1,
"column": 14 "column": 12
}, },
"end": { "end": {
"line": 1, "line": 1,
"column": 26 "column": 13
} }
}, },
"id": null, "name": "a"
"expression": false, }
"generator": false, ]
"async": false, },
"params": [ {
"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", "type": "Identifier",
"start": 15, "start": 34,
"end": 19, "end": 35,
"loc": { "loc": {
"start": { "start": {
"line": 1, "line": 2,
"column": 12
},
"end": {
"line": 2,
"column": 13
}
},
"name": "a"
},
{
"type": "Identifier",
"start": 37,
"end": 38,
"loc": {
"start": {
"line": 2,
"column": 15 "column": 15
}, },
"end": { "end": {
"line": 1, "line": 2,
"column": 16
}
},
"name": "b"
},
{
"type": "Identifier",
"start": 40,
"end": 41,
"loc": {
"start": {
"line": 2,
"column": 18
},
"end": {
"line": 2,
"column": 19 "column": 19
} }
}, },
"name": "node" "name": "c"
}
]
},
{
"type": "Attribute",
"start": 41,
"end": 42,
"name": ")",
"value": true
} }
], ],
"body": { "fragment": {
"type": "BlockStatement", "type": "Fragment",
"start": 24, "nodes": []
"end": 26, }
},
{
"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": { "loc": {
"start": { "start": {
"line": 1, "line": 3,
"column": 12
},
"end": {
"line": 3,
"column": 20
}
},
"argument": {
"type": "Identifier",
"start": 65,
"end": 70,
"loc": {
"start": {
"line": 3,
"column": 15
},
"end": {
"line": 3,
"column": 20
}
},
"name": "stuff"
}
}
]
},
{
"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": 4,
"column": 12
},
"end": {
"line": 4,
"column": 13
}
},
"name": "a"
},
{
"type": "Identifier",
"start": 94,
"end": 95,
"loc": {
"start": {
"line": 4,
"column": 15
},
"end": {
"line": 4,
"column": 16
}
},
"name": "b"
},
{
"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": 103,
"end": 108,
"loc": {
"start": {
"line": 4,
"column": 24 "column": 24
}, },
"end": { "end": {
"line": 1, "line": 4,
"column": 26 "column": 29
} }
}, },
"body": [] "name": "stuff"
}
} }
]
},
{
"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", "type": "AttachTag",
"start": 28, "start": 122,
"end": 50, "end": 141,
"expression": { "attachments": [
{
"type": "ArrowFunctionExpression", "type": "ArrowFunctionExpression",
"start": 37, "start": 129,
"end": 49, "end": 141,
"loc": { "loc": {
"start": { "start": {
"line": 1, "line": 5,
"column": 37 "column": 12
}, },
"end": { "end": {
"line": 1, "line": 5,
"column": 49 "column": 24
} }
}, },
"id": null, "id": null,
@ -96,16 +345,16 @@
"params": [ "params": [
{ {
"type": "Identifier", "type": "Identifier",
"start": 38, "start": 130,
"end": 42, "end": 134,
"loc": { "loc": {
"start": { "start": {
"line": 1, "line": 5,
"column": 38 "column": 13
}, },
"end": { "end": {
"line": 1, "line": 5,
"column": 42 "column": 17
} }
}, },
"name": "node" "name": "node"
@ -113,21 +362,29 @@
], ],
"body": { "body": {
"type": "BlockStatement", "type": "BlockStatement",
"start": 47, "start": 139,
"end": 49, "end": 141,
"loc": { "loc": {
"start": { "start": {
"line": 1, "line": 5,
"column": 47 "column": 22
}, },
"end": { "end": {
"line": 1, "line": 5,
"column": 49 "column": 24
} }
}, },
"body": [] "body": []
} }
} }
]
},
{
"type": "Attribute",
"start": 141,
"end": 142,
"name": ")",
"value": true
} }
], ],
"fragment": { "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'; import Child from './Child.svelte';
</script> </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); let value = $state(1);
</script> </script>
<div {@attach (node) => node.textContent = value}></div> <div attach((node) => node.textContent = value)></div>
<button onclick={() => value += 1}>increment</button> <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' { 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 { SourceMap } from 'magic-string';
import type { Location } from 'locate-character'; import type { Location } from 'locate-character';
/** /**
@ -1053,7 +1053,7 @@ declare module 'svelte/compiler' {
/** A `{@attach foo(...)} tag */ /** A `{@attach foo(...)} tag */
export interface AttachTag extends BaseNode { export interface AttachTag extends BaseNode {
type: 'AttachTag'; type: 'AttachTag';
expression: Expression; attachments: Array<Expression | SpreadElement>;
} }
/** An `animate:` directive */ /** An `animate:` directive */

Loading…
Cancel
Save