pull/15538/head
Rich Harris 4 months ago
parent d9237e25bc
commit 17f1b6af30

@ -76,8 +76,8 @@ export function transform_template(state, context, namespace, template_name, fla
/** @type {Expression[]} */ /** @type {Expression[]} */
const args = [ const args = [
state.options.templatingMode === 'functional' state.options.templatingMode === 'functional'
? template_to_functions(state.template) ? template_to_functions(state.template.nodes)
: b.template([b.quasi(template_to_string(state.template), true)], []) : b.template([b.quasi(template_to_string(state.template.nodes), true)], [])
]; ];
if (flags) { if (flags) {

@ -0,0 +1,95 @@
/** @import { AST } from '#compiler' */
/** @import { TemplateOperation } from '../types.js' */
/** @import { Node, Element } from './types'; */
export class Template {
/** @type {Node[]} */
nodes = [];
/** @type {Node[][]} */
#stack = [this.nodes];
/** @type {Element | undefined} */
#element;
#fragment = this.nodes;
/**
* @param {...TemplateOperation} nodes
* @deprecated
*/
push(...nodes) {
for (const node of nodes) {
switch (node.kind) {
case 'create_element':
this.create_element(node.name);
break;
case 'create_anchor':
this.create_anchor(node.data);
break;
case 'create_text':
this.create_text(node.nodes);
break;
case 'push_element': {
this.push_element();
break;
}
case 'pop_element': {
this.pop_element();
break;
}
case 'set_prop': {
this.set_prop(node.key, node.value);
break;
}
}
}
}
/** @param {string} name */
create_element(name) {
this.#element = {
type: 'element',
name,
attributes: {},
children: []
};
this.#fragment.push(this.#element);
}
/** @param {string | undefined} data */
create_anchor(data) {
this.#fragment.push({ type: 'anchor', data });
}
/** @param {AST.Text[]} nodes */
create_text(nodes) {
this.#fragment.push({ type: 'text', nodes });
}
push_element() {
const element = /** @type {Element} */ (this.#element);
this.#fragment = element.children;
this.#stack.push(this.#fragment);
}
pop_element() {
this.#stack.pop();
this.#fragment = /** @type {Node[]} */ (this.#stack.at(-1));
}
/**
* @param {string} key
* @param {string | undefined} value
*/
set_prop(key, value) {
const element = /** @type {Element} */ (this.#element);
element.attributes[key] = value;
}
}

@ -1,70 +1,52 @@
/** @import { TemplateOperation } from '../types.js' */ /** @import { Node } from './types.js' */
/** @import { ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from 'estree' */ /** @import { ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from 'estree' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { regex_is_valid_identifier, regex_starts_with_newline } from '../../../patterns.js'; import { regex_is_valid_identifier, regex_starts_with_newline } from '../../../patterns.js';
import fix_attribute_casing from './fix-attribute-casing.js'; import fix_attribute_casing from './fix-attribute-casing.js';
/** /**
* @param {TemplateOperation[]} items * @param {Node[]} items
*/ */
export function template_to_functions(items) { export function template_to_functions(items) {
let elements = b.array([]); return b.array(items.map(build));
}
/** /** @param {Node} item */
* @type {Array<Element>} function build(item) {
*/ switch (item.type) {
let elements_stack = []; case 'element': {
const element = b.object([b.prop('init', b.id('e'), b.literal(item.name))]);
const entries = Object.entries(item.attributes);
if (entries.length > 0) {
element.properties.push(
b.prop(
'init',
b.id('p'),
b.object(
entries.map(([key, value]) => {
return b.prop('init', b.key(key), value === undefined ? b.void0 : b.literal(value));
})
)
)
);
}
/** if (item.children.length > 0) {
* @type {Element | undefined} element.properties.push(b.prop('init', b.id('c'), b.array(item.children.map(build))));
*/ }
let last_current_element;
// if the first item is a comment we need to add another comment for effect.start return element;
if (items[0].kind === 'create_anchor') { }
items.unshift({ kind: 'create_anchor' });
}
for (let instruction of items) { case 'anchor': {
const last_element_stack = /** @type {Element} */ (elements_stack.at(-1)); return item.data ? b.array([b.literal(item.data)]) : null;
/**
* @param {Expression | null | void} value
* @returns
*/
function push(value) {
if (value === undefined) return;
if (last_element_stack) {
insert(last_element_stack, value);
} else {
elements.elements.push(value);
}
} }
switch (instruction.kind) { case 'text': {
case 'push_element': return b.literal(item.nodes.map((node) => node.data).join(','));
elements_stack.push(/** @type {Element} */ (last_current_element));
break;
case 'pop_element':
elements_stack.pop();
last_current_element = elements_stack.at(-1);
break;
case 'create_element':
last_current_element = create_element(instruction.name);
push(last_current_element);
break;
case 'create_text':
push(create_text(last_element_stack, instruction.nodes.map((node) => node.data).join('')));
break;
case 'create_anchor':
push(create_anchor(last_element_stack, instruction.data));
break;
case 'set_prop':
set_prop(/** @type {Element} */ (last_current_element), instruction.key, instruction.value);
break;
} }
} }
return elements;
} }
/** /**

@ -1,95 +1,14 @@
/** @import { TemplateOperation } from '../types.js' */ /** @import { Node } from './types.js' */
import { escape_html } from '../../../../../escaping.js'; import { escape_html } from '../../../../../escaping.js';
import { is_void } from '../../../../../utils.js'; import { is_void } from '../../../../../utils.js';
/** /**
* @param {TemplateOperation[]} items * @param {Node[]} items
*/ */
export function template_to_string(items) { export function template_to_string(items) {
/** return items.map((el) => stringify(el)).join('');
* @type {Array<Element>}
*/
let elements = [];
/**
* @type {Array<Element>}
*/
let elements_stack = [];
/**
* @type {Element | undefined}
*/
let last_current_element;
/**
* @template {Node} T
* @param {T} child
*/
function insert(child) {
if (last_current_element) {
last_current_element.children ??= [];
last_current_element.children.push(child);
} else {
elements.push(/** @type {Element} */ (child));
}
return child;
}
for (let instruction of items) {
switch (instruction.kind) {
case 'push_element':
elements_stack.push(/** @type {Element} */ (last_current_element));
break;
case 'pop_element':
elements_stack.pop();
last_current_element = elements_stack.at(-1);
break;
case 'create_element':
last_current_element = insert({
kind: 'element',
element: instruction.name
});
break;
case 'create_text':
insert({
kind: 'text',
value: instruction.nodes.map((node) => node.raw).join('')
});
break;
case 'create_anchor':
insert({
kind: 'anchor',
data: instruction.data
});
break;
case 'set_prop': {
const el = /** @type {Element} */ (last_current_element);
el.props ??= {};
el.props[instruction.key] = escape_html(instruction.value, true);
break;
}
}
}
return elements.map((el) => stringify(el)).join('');
} }
/**
* @typedef {{ kind: "element", element: string, props?: Record<string, string>, children?: Array<Node> }} Element
*/
/**
* @typedef {{ kind: "anchor", data?: string }} Anchor
*/
/**
* @typedef {{ kind: "text", value?: string }} Text
*/
/**
* @typedef { Element | Anchor| Text } Node
*/
/** /**
* *
* @param {Node} el * @param {Node} el
@ -97,15 +16,15 @@ export function template_to_string(items) {
*/ */
function stringify(el) { function stringify(el) {
let str = ``; let str = ``;
if (el.kind === 'element') { if (el.type === 'element') {
// we create the <tagname part // we create the <tagname part
str += `<${el.element}`; str += `<${el.name}`;
// we concatenate all the prop to it // we concatenate all the prop to it
for (let [prop, value] of Object.entries(el.props ?? {})) { for (let [prop, value] of Object.entries(el.attributes ?? {})) {
if (value == null) { if (value == null) {
str += ` ${prop}`; str += ` ${prop}`;
} else { } else {
str += ` ${prop}="${value}"`; str += ` ${prop}="${escape_html(value, true)}"`;
} }
} }
// then we close the opening tag // then we close the opening tag
@ -115,12 +34,12 @@ function stringify(el) {
str += stringify(child); str += stringify(child);
} }
// if it's not void we also add the closing tag // if it's not void we also add the closing tag
if (!is_void(el.element)) { if (!is_void(el.name)) {
str += `</${el.element}>`; str += `</${el.name}>`;
} }
} else if (el.kind === 'text') { } else if (el.type === 'text') {
str += el.value; str += el.nodes.map((node) => node.raw).join('');
} else if (el.kind === 'anchor') { } else if (el.type === 'anchor') {
if (el.data) { if (el.data) {
str += `<!--${el.data}-->`; str += `<!--${el.data}-->`;
} else { } else {

@ -0,0 +1,20 @@
import type { AST } from '#compiler';
export interface Element {
type: 'element';
name: string;
attributes: Record<string, string | undefined>;
children: Node[];
}
export interface Text {
type: 'text';
nodes: AST.Text[];
}
export interface Anchor {
type: 'anchor';
data: string | undefined;
}
export type Node = Element | Text | Anchor;

@ -13,6 +13,7 @@ import type { AST, Namespace, StateField, ValidatedCompileOptions } from '#compi
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared'; import type { SourceLocation } from '#shared';
import type { Template } from './transform-template/template.js';
export interface ClientTransformState extends TransformState { export interface ClientTransformState extends TransformState {
/** /**
@ -78,7 +79,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Expressions used inside the render effect */ /** Expressions used inside the render effect */
readonly expressions: Expression[]; readonly expressions: Expression[];
/** The HTML template string */ /** The HTML template string */
readonly template: TemplateOperation[]; readonly template: Template;
readonly locations: SourceLocation[]; readonly locations: SourceLocation[];
readonly metadata: { readonly metadata: {
namespace: Namespace; namespace: Namespace;

@ -7,6 +7,7 @@ import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js'; import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js'; import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js'; import { build_render_statement } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/** /**
* @param {AST.Fragment} node * @param {AST.Fragment} node
@ -65,7 +66,7 @@ export function Fragment(node, context) {
update: [], update: [],
expressions: [], expressions: [],
after_update: [], after_update: [],
template: [], template: new Template(),
locations: [], locations: [],
transform: { ...context.state.transform }, transform: { ...context.state.transform },
metadata: { metadata: {
@ -147,7 +148,7 @@ export function Fragment(node, context) {
flags |= TEMPLATE_USE_IMPORT_NODE; flags |= TEMPLATE_USE_IMPORT_NODE;
} }
if (state.template.length === 1 && state.template[0].kind === 'create_anchor') { if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'anchor') {
// special case — we can use `$.comment` instead of creating a unique template // special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment'))); body.push(b.var(id, b.call('$.comment')));
} else { } else {

@ -1,6 +1,6 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */ /** @import { ComponentContext, TemplateOperation } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
@ -440,10 +440,9 @@ export function build_component(node, component_name, context, anchor = context.
} }
if (Object.keys(custom_css_props).length > 0) { if (Object.keys(custom_css_props).length > 0) {
/** /** @type {TemplateOperation[]} */
* @type {typeof context.state.template}
*/
const template_operations = []; const template_operations = [];
if (context.state.metadata.namespace === 'svg') { if (context.state.metadata.namespace === 'svg') {
// this boils down to <g><!></g> // this boils down to <g><!></g>
template_operations.push({ kind: 'create_element', name: 'g' }); template_operations.push({ kind: 'create_element', name: 'g' });

Loading…
Cancel
Save