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[]} */
const args = [
state.options.templatingMode === 'functional'
? template_to_functions(state.template)
: b.template([b.quasi(template_to_string(state.template), true)], [])
? template_to_functions(state.template.nodes)
: b.template([b.quasi(template_to_string(state.template.nodes), true)], [])
];
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 * as b from '../../../../utils/builders.js';
import { regex_is_valid_identifier, regex_starts_with_newline } from '../../../patterns.js';
import fix_attribute_casing from './fix-attribute-casing.js';
/**
* @param {TemplateOperation[]} items
* @param {Node[]} items
*/
export function template_to_functions(items) {
let elements = b.array([]);
return b.array(items.map(build));
}
/**
* @type {Array<Element>}
*/
let elements_stack = [];
/** @param {Node} item */
function build(item) {
switch (item.type) {
case 'element': {
const element = b.object([b.prop('init', b.id('e'), b.literal(item.name))]);
/**
* @type {Element | undefined}
*/
let last_current_element;
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 the first item is a comment we need to add another comment for effect.start
if (items[0].kind === 'create_anchor') {
items.unshift({ kind: 'create_anchor' });
if (item.children.length > 0) {
element.properties.push(b.prop('init', b.id('c'), b.array(item.children.map(build))));
}
for (let instruction of items) {
const last_element_stack = /** @type {Element} */ (elements_stack.at(-1));
/**
* @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);
return element;
}
case 'anchor': {
return item.data ? b.array([b.literal(item.data)]) : null;
}
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 = 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;
case 'text': {
return b.literal(item.nodes.map((node) => node.data).join(','));
}
}
return elements;
}
/**

@ -1,95 +1,14 @@
/** @import { TemplateOperation } from '../types.js' */
/** @import { Node } from './types.js' */
import { escape_html } from '../../../../../escaping.js';
import { is_void } from '../../../../../utils.js';
/**
* @param {TemplateOperation[]} items
* @param {Node[]} items
*/
export function template_to_string(items) {
/**
* @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('');
return items.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
@ -97,15 +16,15 @@ export function template_to_string(items) {
*/
function stringify(el) {
let str = ``;
if (el.kind === 'element') {
if (el.type === 'element') {
// we create the <tagname part
str += `<${el.element}`;
str += `<${el.name}`;
// 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) {
str += ` ${prop}`;
} else {
str += ` ${prop}="${value}"`;
str += ` ${prop}="${escape_html(value, true)}"`;
}
}
// then we close the opening tag
@ -115,12 +34,12 @@ function stringify(el) {
str += stringify(child);
}
// if it's not void we also add the closing tag
if (!is_void(el.element)) {
str += `</${el.element}>`;
if (!is_void(el.name)) {
str += `</${el.name}>`;
}
} else if (el.kind === 'text') {
str += el.value;
} else if (el.kind === 'anchor') {
} else if (el.type === 'text') {
str += el.nodes.map((node) => node.raw).join('');
} else if (el.type === 'anchor') {
if (el.data) {
str += `<!--${el.data}-->`;
} 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 { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
import type { Template } from './transform-template/template.js';
export interface ClientTransformState extends TransformState {
/**
@ -78,7 +79,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Expressions used inside the render effect */
readonly expressions: Expression[];
/** The HTML template string */
readonly template: TemplateOperation[];
readonly template: Template;
readonly locations: SourceLocation[];
readonly metadata: {
namespace: Namespace;

@ -7,6 +7,7 @@ import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
* @param {AST.Fragment} node
@ -65,7 +66,7 @@ export function Fragment(node, context) {
update: [],
expressions: [],
after_update: [],
template: [],
template: new Template(),
locations: [],
transform: { ...context.state.transform },
metadata: {
@ -147,7 +148,7 @@ export function Fragment(node, context) {
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
body.push(b.var(id, b.call('$.comment')));
} else {

@ -1,6 +1,6 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
/** @import { ComponentContext, TemplateOperation } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
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) {
/**
* @type {typeof context.state.template}
*/
/** @type {TemplateOperation[]} */
const template_operations = [];
if (context.state.metadata.namespace === 'svg') {
// this boils down to <g><!></g>
template_operations.push({ kind: 'create_element', name: 'g' });

Loading…
Cancel
Save