Merge branch 'main' into svelte-custom-renderer

svelte-custom-renderer
paoloricciuti 4 weeks ago
commit dd148ebb74

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply CSS custom properties with falsy values on components

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: increment private state fields through a non-`this` receiver

@ -1,5 +1,17 @@
# svelte
## 5.56.9
### Patch Changes
- fix: skip controlled each fast path while another batch is pending ([#18625](https://github.com/sveltejs/svelte/pull/18625))
- fix: better whitespace handling inside printer ([#18638](https://github.com/sveltejs/svelte/pull/18638))
- fix: don't duplicate comments in attributes ([#18636](https://github.com/sveltejs/svelte/pull/18636))
- fix: preserve CSS comments in the AST printer ([#18637](https://github.com/sveltejs/svelte/pull/18637))
## 5.56.8
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.56.8",
"version": "5.56.9",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -176,7 +176,8 @@ export function parseCss(source) {
type: 'StyleSheetFile',
start: 0,
end: source.length,
children
children,
comments: parser.css_comments
};
}

@ -50,6 +50,9 @@ export class Parser {
/** */
index = 0;
/** @type {AST.CSS.CSSComment[]} */
css_comments = [];
/**
* Creates a minimal parser instance for CSS-only parsing.
* Skips Svelte component parsing setup.
@ -61,6 +64,7 @@ export class Parser {
parser.template = source;
parser.index = 0;
parser.loose = false;
parser.css_comments = [];
return parser;
}

@ -24,6 +24,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
parser.css_comments = [];
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
const content_end = parser.index;
@ -36,6 +37,7 @@ export default function read_style(parser, start, attributes) {
end: parser.index,
attributes,
children,
comments: parser.css_comments,
content: {
start: content_start,
end: content_end,
@ -229,18 +231,22 @@ function read_selector(parser, inside_pseudo_class = false) {
end: parser.index
});
} else if (parser.eat('::')) {
const name = read_identifier(parser);
/** @type {AST.CSS.SelectorList | null} */
let args = null;
if (parser.eat('(')) {
args = read_selector_list(parser, true);
parser.eat(')', true);
}
relative_selector.selectors.push({
type: 'PseudoElementSelector',
name: read_identifier(parser),
name,
start,
end: parser.index
end: parser.index,
...(args && { args })
});
// We read the inner selectors of a pseudo element to ensure it parses correctly,
// but we don't do anything with the result.
if (parser.eat('(')) {
read_selector_list(parser, true);
parser.eat(')', true);
}
} else if (parser.eat(':')) {
const name = read_identifier(parser);
@ -323,7 +329,7 @@ function read_selector(parser, inside_pseudo_class = false) {
}
const index = parser.index;
allow_comment_or_whitespace(parser);
allow_comment_or_whitespace(parser, false);
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list
@ -449,7 +455,7 @@ function read_block_item(parser) {
// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
read_value(parser);
read_value(parser, false);
const char = parser.template[parser.index];
parser.index = start;
@ -492,10 +498,13 @@ function read_declaration(parser) {
/**
* @param {Parser} parser
* @param {boolean} [capture_comments]
* @returns {string}
*/
function read_value(parser) {
function read_value(parser, capture_comments = true) {
let value = '';
/** @type {AST.CSS.CSSComment[]} */
const value_comments = [];
let escaped = false;
let in_url = false;
@ -523,6 +532,13 @@ function read_value(parser) {
} else if (char === '(' && value.slice(-3) === 'url') {
in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
const leading_whitespace = value.length - value.trimStart().length;
for (const comment of value_comments) {
comment.position = Math.max(
0,
/** @type {number} */ (comment.position) - leading_whitespace
);
}
return value.trim();
} else if (
char === '/' &&
@ -530,13 +546,11 @@ function read_value(parser) {
!quote_mark &&
parser.template[parser.index + 1] === '*'
) {
parser.index += 2;
while (parser.index < parser.template.length) {
if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') {
parser.index += 2;
break;
}
parser.index++;
const comment = read_comment(parser);
if (capture_comments) {
comment.position = value.length;
parser.css_comments.push(comment);
value_comments.push(comment);
}
continue;
}
@ -624,13 +638,16 @@ function read_identifier(parser) {
return identifier;
}
/** @param {Parser} parser */
function allow_comment_or_whitespace(parser) {
/**
* @param {Parser} parser
* @param {boolean} [capture_comments]
*/
function allow_comment_or_whitespace(parser, capture_comments = true) {
parser.allow_whitespace();
while (parser.match('/*') || parser.match('<!--')) {
if (parser.eat('/*')) {
parser.read_until(REGEX_COMMENT_CLOSE);
parser.eat('*/', true);
if (parser.match('/*')) {
const comment = read_comment(parser);
if (capture_comments) parser.css_comments.push(comment);
}
if (parser.eat('<!--')) {
@ -642,6 +659,25 @@ function allow_comment_or_whitespace(parser) {
}
}
/**
* @param {Parser} parser
* @returns {AST.CSS.CSSComment}
*/
function read_comment(parser) {
const start = parser.index;
parser.eat('/*', true);
const value = parser.read_until(REGEX_COMMENT_CLOSE);
parser.eat('*/', true);
const end = parser.index;
return {
type: 'CSSComment',
value,
start,
end
};
}
/**
* Parse standalone CSS content (not wrapped in `<style>`).
* @param {Parser} parser

@ -312,6 +312,9 @@ const css_visitors = {
}
context.next();
},
PseudoElementSelector() {
// Don't analyse these
}
};

@ -23,6 +23,9 @@ const visitors = {
context.next();
}
},
PseudoElementSelector() {
// Don't analyse these
},
ComplexSelector(node, context) {
if (
!node.metadata.used &&

@ -13,7 +13,6 @@ export function UpdateExpression(node, context) {
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
argument.property.type === 'PrivateIdentifier' &&
context.state.state_fields.has('#' + argument.property.name)
) {

@ -374,6 +374,9 @@ const visitors = {
if (node.name === 'is' || node.name === 'where' || node.name === 'has' || node.name === 'not') {
context.next();
}
},
PseudoElementSelector() {
// Functional pseudo-element arguments are not scoped as selectors.
}
};

@ -19,6 +19,10 @@ const LINE_BREAK_THRESHOLD = 50;
*/
export function print(ast, options = undefined) {
const comments = (ast.type === 'Root' && ast.comments) || [];
const state = { preserve_whitespace: 0 };
const css_comments =
(ast.type === 'Root' ? ast.css?.comments : ast.type === 'StyleSheet' ? ast.comments : null) ||
[];
return esrap.print(
ast,
@ -28,8 +32,8 @@ export function print(ast, options = undefined) {
getLeadingComments: options?.getLeadingComments,
getTrailingComments: options?.getTrailingComments
}),
...svelte_visitors(comments),
...css_visitors
...svelte_visitors(comments, state),
...css_visitors(css_comments, comments)
}),
{
indent: options?.indent
@ -40,9 +44,15 @@ export function print(ast, options = undefined) {
/**
* @param {Context} context
* @param {AST.SvelteNode} node
* @param {boolean} preserve_whitespace
* @param {boolean} allow_inline
*/
function block(context, node, allow_inline = false) {
function block(context, node, preserve_whitespace = false, allow_inline = false) {
if (preserve_whitespace) {
context.visit(node);
return;
}
const child_context = context.new();
child_context.visit(node);
@ -82,6 +92,7 @@ function attributes(node, attributes, context, comments) {
}
const separator = context.new();
let previous_attribute_end = node.start;
const children = attributes.map((attribute) => {
const child_context = context.new();
@ -90,12 +101,16 @@ function attributes(node, attributes, context, comments) {
const comment = comments[comment_index];
if (comment.start < attribute.start) {
if (comment.type === 'Line') {
child_context.write('//' + comment.value);
child_context.newline();
} else {
child_context.write('/*' + comment.value + '*/'); // TODO match indentation?
child_context.append(separator);
// Inside a previous attribute's value can be comments which don't
// advance comment_index, therefore this additional check
if (comment.start >= previous_attribute_end) {
if (comment.type === 'Line') {
child_context.write('//' + comment.value);
child_context.newline();
} else {
child_context.write('/*' + comment.value + '*/'); // TODO match indentation?
child_context.append(separator);
}
}
comment_index += 1;
@ -105,6 +120,7 @@ function attributes(node, attributes, context, comments) {
}
child_context.visit(attribute);
previous_attribute_end = attribute.end;
length += child_context.measure() + 1;
@ -137,8 +153,9 @@ function attributes(node, attributes, context, comments) {
* @param {AST.BaseElement} node
* @param {Context} context
* @param {AST.JSComment[]} comments
* @param {{ preserve_whitespace: number }} state
*/
function base_element(node, context, comments) {
function base_element(node, context, comments, state) {
const child_context = context.new();
child_context.write('<' + node.name);
@ -164,174 +181,294 @@ function base_element(node, context, comments) {
child_context.write(`${multiline_attributes ? '' : ' '}/>`);
} else {
child_context.write('>');
block(child_context, node.fragment, true);
block(child_context, node.fragment, state.preserve_whitespace > 0, true);
child_context.write(`</${node.name}>`);
}
context.append(child_context);
}
/** @type {Visitors<AST.SvelteNode>} */
const css_visitors = {
Atrule(node, context) {
context.write(`@${node.name}`);
if (node.prelude) context.write(` ${node.prelude}`);
/**
* @param {AST.BaseElement} node
* @param {Context} context
* @param {AST.JSComment[]} comments
* @param {{ preserve_whitespace: number }} state
*/
function print_element(node, context, comments, state) {
const name = node.name.toLowerCase();
const preserve =
(node.type === 'RegularElement' || node.type === 'TitleElement') &&
(name === 'pre' || name === 'textarea' || name === 'title');
if (preserve) state.preserve_whitespace += 1;
base_element(node, context, comments, state);
if (preserve) state.preserve_whitespace -= 1;
}
if (node.block) {
context.write(' ');
context.visit(node.block);
} else {
context.write(';');
}
},
/**
* @param {AST.CSS.CSSComment[]} comments
* @param {AST.JSComment[]} js_comments
* @returns {Visitors<AST.SvelteNode>}
*/
function css_visitors(comments, js_comments) {
let comment_index = 0;
/** @param {number} end */
const has_comment_before = (end) => comments[comment_index]?.start < end;
/**
* @param {Context} context
* @param {AST.CSS.CSSComment} comment
*/
function write_comment(context, comment) {
context.write(`/*${comment.value}*/`);
}
AttributeSelector(node, context) {
context.write(`[${node.name}`);
if (node.matcher) {
context.write(node.matcher);
context.write(`"${node.value}"`);
if (node.flags) {
context.write(` ${node.flags}`);
}
/**
* @param {Context} context
* @param {number} end
*/
function write_inline_comments(context, end) {
let written = false;
while (has_comment_before(end)) {
if (written) context.write(' ');
write_comment(context, comments[comment_index++]);
written = true;
}
context.write(']');
},
Block(node, context) {
context.write('{');
return written;
}
if (node.children.length > 0) {
context.indent();
context.newline();
/**
* @param {Context} context
* @param {string} value
* @param {number} end
*/
function write_value(context, value, end) {
let offset = 0;
while (has_comment_before(end)) {
const comment = comments[comment_index++];
const position = Math.max(offset, Math.min(comment.position ?? 0, value.length));
context.write(value.slice(offset, position));
write_comment(context, comment);
offset = position;
}
let started = false;
context.write(value.slice(offset));
}
for (const child of node.children) {
if (started) {
context.newline();
}
/**
* @param {Context} context
* @param {Array<AST.CSS.Rule | AST.CSS.Atrule | AST.CSS.Declaration>} children
* @param {number} end
* @param {boolean} margins
*/
function print_children(context, children, end, margins) {
let started = false;
context.visit(child);
const separate = () => {
if (!started) return;
if (margins) context.margin();
context.newline();
};
for (const child of children) {
while (has_comment_before(child.start)) {
separate();
write_comment(context, comments[comment_index++]);
started = true;
}
context.dedent();
context.newline();
separate();
context.visit(child);
started = true;
}
context.write('}');
},
while (has_comment_before(end)) {
separate();
write_comment(context, comments[comment_index++]);
started = true;
}
}
ClassSelector(node, context) {
context.write(`.${node.name}`);
},
/**
* @param {AST.CSS.SelectorList} node
* @param {Context} context
* @param {boolean} multiline
*/
function print_selector_list(node, context, multiline) {
let needs_separator = false;
let remaining_selectors = node.children.length;
ComplexSelector(node, context) {
for (const selector of node.children) {
while (has_comment_before(selector.start)) {
if (needs_separator) context.write(' ');
write_comment(context, comments[comment_index++]);
needs_separator = true;
}
if (needs_separator) {
if (multiline) context.newline();
else context.write(' ');
}
context.visit(selector);
needs_separator = true;
remaining_selectors -= 1;
if (remaining_selectors > 0) context.write(',');
}
},
}
Declaration(node, context) {
context.write(`${node.property}: ${node.value};`);
},
return {
Atrule(node, context) {
context.write(`@${node.name}`);
IdSelector(node, context) {
context.write(`#${node.name}`);
},
const prelude_end = node.block?.start ?? node.end;
if (node.prelude || has_comment_before(prelude_end)) {
context.write(' ');
write_value(context, node.prelude, prelude_end);
}
NestingSelector(node, context) {
context.write('&');
},
if (node.block) {
context.write(' ');
context.visit(node.block);
} else {
context.write(';');
}
},
AttributeSelector(node, context) {
context.write(`[${node.name}`);
if (node.matcher) {
context.write(node.matcher);
context.write(`"${node.value}"`);
if (node.flags) context.write(` ${node.flags}`);
}
context.write(']');
},
Nth(node, context) {
context.write(node.value);
},
Block(node, context) {
context.write('{');
Percentage(node, context) {
context.write(node.value);
},
if (node.children.length > 0 || has_comment_before(node.end)) {
context.indent();
context.newline();
print_children(context, node.children, node.end, false);
context.dedent();
context.newline();
}
PseudoClassSelector(node, context) {
context.write(`:${node.name}`);
context.write('}');
},
if (node.args) {
context.write('(');
ClassSelector(node, context) {
context.write(`.${node.name}`);
},
let started = false;
ComplexSelector(node, context) {
for (const selector of node.children) context.visit(selector);
},
for (const arg of node.args.children) {
if (started) {
context.write(', ');
}
Declaration(node, context) {
context.write(`${node.property}: `);
write_value(context, node.value, node.end);
context.write(';');
},
context.visit(arg);
IdSelector(node, context) {
context.write(`#${node.name}`);
},
started = true;
}
NestingSelector(node, context) {
context.write('&');
},
context.write(')');
}
},
Nth(node, context) {
context.write(node.value);
},
PseudoElementSelector(node, context) {
context.write(`::${node.name}`);
},
Percentage(node, context) {
context.write(node.value);
},
RelativeSelector(node, context) {
if (node.combinator) {
if (node.combinator.name === ' ') {
context.write(' ');
} else {
context.write(` ${node.combinator.name} `);
PseudoClassSelector(node, context) {
context.write(`:${node.name}`);
if (node.args) {
context.write('(');
context.visit(node.args);
if (has_comment_before(node.end)) {
context.write(' ');
write_inline_comments(context, node.end);
}
context.write(')');
}
}
},
PseudoElementSelector(node, context) {
context.write(`::${node.name}`);
if (node.args) {
context.write('(');
context.visit(node.args);
if (has_comment_before(node.end)) {
context.write(' ');
write_inline_comments(context, node.end);
}
context.write(')');
}
},
for (const selector of node.selectors) {
context.visit(selector);
}
},
RelativeSelector(node, context) {
if (node.combinator) {
if (node.combinator.name === ' ') context.write(' ');
else context.write(` ${node.combinator.name} `);
}
Rule(node, context) {
let started = false;
for (const selector of node.selectors) context.visit(selector);
},
for (const selector of node.prelude.children) {
if (started) {
context.write(',');
context.newline();
}
Rule(node, context) {
print_selector_list(node.prelude, context, true);
context.write(' ');
if (write_inline_comments(context, node.block.start)) context.write(' ');
context.visit(node.block);
},
context.visit(selector);
started = true;
}
SelectorList(node, context) {
print_selector_list(node, context, false);
},
context.write(' ');
context.visit(node.block);
},
StyleSheet(node, context) {
context.write('<style');
attributes(node, node.attributes, context, js_comments);
context.write('>');
SelectorList(node, context) {
let started = false;
for (const selector of node.children) {
if (started) {
context.write(', ');
if (node.children.length > 0 || node.comments.length > 0) {
context.indent();
context.newline();
print_children(context, node.children, node.content.end, true);
context.dedent();
context.newline();
}
context.visit(selector);
started = true;
}
},
context.write('</style>');
},
TypeSelector(node, context) {
context.write(node.name);
}
};
TypeSelector(node, context) {
context.write(node.name);
}
};
}
/**
* @param {AST.JSComment[]} comments
* @param {{ preserve_whitespace: number }} state
* @returns {Visitors<AST.SvelteNode>}
*/
const svelte_visitors = (comments) => ({
const svelte_visitors = (comments, state) => ({
Root(node, context) {
if (node.options) {
context.write('<svelte:options');
@ -363,11 +500,27 @@ const svelte_visitors = (comments) => ({
context.write('<script');
attributes(node, node.attributes, context, comments);
context.write('>');
block(context, node.content);
block(context, node.content, state.preserve_whitespace > 0);
context.write('</script>');
},
Fragment(node, context) {
if (state.preserve_whitespace > 0) {
for (const child of node.nodes) {
context.visit(child);
context.multiline ||= child.type === 'Text' && /[\r\n]/.test(child.data);
}
return;
}
const first = node.nodes[0];
const last = node.nodes.at(-1);
const has_surrounding_whitespace =
first?.type === 'Text' &&
/^\s/.test(first.data) &&
last?.type === 'Text' &&
/\s$/.test(last.data);
/** @type {AST.SvelteNode[][]} */
const items = [];
@ -459,6 +612,10 @@ const svelte_visitors = (comments) => ({
});
multiline ||= width > LINE_BREAK_THRESHOLD;
// Normally context.newline() also makes context.multiline true, but the below loop only
// does that if we have more than one child context. If there's one long text block inside
// with whitespace at the edges we wanna split that up, too.
context.multiline ||= has_surrounding_whitespace && width > LINE_BREAK_THRESHOLD * 2;
for (let i = 0; i < child_contexts.length; i += 1) {
const prev = child_contexts[i];
@ -525,7 +682,7 @@ const svelte_visitors = (comments) => ({
if (node.pending) {
context.write('}');
block(context, node.pending);
block(context, node.pending, state.preserve_whitespace > 0);
context.write('{:');
} else {
context.write(' ');
@ -536,7 +693,7 @@ const svelte_visitors = (comments) => ({
if (node.value) context.visit(node.value);
context.write('}');
block(context, node.then);
block(context, node.then, state.preserve_whitespace > 0);
if (node.catch) {
context.write('{:');
@ -548,7 +705,7 @@ const svelte_visitors = (comments) => ({
if (node.error) context.visit(node.error);
context.write('}');
block(context, node.catch);
block(context, node.catch, state.preserve_whitespace > 0);
}
context.write('{/await}');
@ -592,7 +749,7 @@ const svelte_visitors = (comments) => ({
},
Component(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
ConstTag(node, context) {
@ -682,11 +839,11 @@ const svelte_visitors = (comments) => ({
context.write('}');
block(context, node.body);
block(context, node.body, state.preserve_whitespace > 0);
if (node.fallback) {
context.write('{:else}');
block(context, node.fallback);
block(context, node.fallback, state.preserve_whitespace > 0);
}
context.write('{/each}');
@ -710,13 +867,13 @@ const svelte_visitors = (comments) => ({
context.visit(node.test);
context.write('}');
block(context, node.consequent);
block(context, node.consequent, state.preserve_whitespace > 0);
} else {
context.write('{#if ');
context.visit(node.test);
context.write('}');
block(context, node.consequent);
block(context, node.consequent, state.preserve_whitespace > 0);
}
if (node.alternate !== null) {
@ -728,7 +885,7 @@ const svelte_visitors = (comments) => ({
)
) {
context.write('{:else}');
block(context, node.alternate);
block(context, node.alternate, state.preserve_whitespace > 0);
} else {
context.visit(node.alternate);
}
@ -743,7 +900,7 @@ const svelte_visitors = (comments) => ({
context.write('{#key ');
context.visit(node.expression);
context.write('}');
block(context, node.fragment);
block(context, node.fragment, state.preserve_whitespace > 0);
context.write('{/key}');
},
@ -775,7 +932,7 @@ const svelte_visitors = (comments) => ({
},
RegularElement(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
RenderTag(node, context) {
@ -785,7 +942,7 @@ const svelte_visitors = (comments) => ({
},
SlotElement(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SnippetBlock(node, context) {
@ -804,7 +961,7 @@ const svelte_visitors = (comments) => ({
}
context.write(')}');
block(context, node.body);
block(context, node.body, state.preserve_whitespace > 0);
context.write('{/snippet}');
},
@ -839,40 +996,12 @@ const svelte_visitors = (comments) => ({
}
},
StyleSheet(node, context) {
context.write('<style');
attributes(node, node.attributes, context, comments);
context.write('>');
if (node.children.length > 0) {
context.indent();
context.newline();
let started = false;
for (const child of node.children) {
if (started) {
context.margin();
context.newline();
}
context.visit(child);
started = true;
}
context.dedent();
context.newline();
}
context.write('</style>');
},
SvelteBody(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SvelteBoundary(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SvelteComponent(node, context) {
@ -884,7 +1013,7 @@ const svelte_visitors = (comments) => ({
attributes(node, node.attributes, context, comments);
if (node.fragment && node.fragment.nodes.length > 0) {
context.write('>');
block(context, node.fragment, true);
block(context, node.fragment, state.preserve_whitespace > 0, true);
context.write(`</svelte:component>`);
} else {
context.write(' />');
@ -892,7 +1021,7 @@ const svelte_visitors = (comments) => ({
},
SvelteDocument(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SvelteElement(node, context) {
@ -905,7 +1034,7 @@ const svelte_visitors = (comments) => ({
if (node.fragment && node.fragment.nodes.length > 0) {
context.write('>');
block(context, node.fragment);
block(context, node.fragment, state.preserve_whitespace > 0);
context.write(`</svelte:element>`);
} else {
context.write(' />');
@ -913,19 +1042,19 @@ const svelte_visitors = (comments) => ({
},
SvelteFragment(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SvelteHead(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SvelteSelf(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
SvelteWindow(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
Text(node, context) {
@ -933,7 +1062,7 @@ const svelte_visitors = (comments) => ({
},
TitleElement(node, context) {
base_element(node, context, comments);
print_element(node, context, comments, state);
},
TransitionDirective(node, context) {

@ -8,6 +8,15 @@ export namespace _CSS {
export interface StyleSheetBase extends BaseNode {
children: Array<Atrule | Rule>;
/** CSS comments in source order */
comments: CSSComment[];
}
export interface CSSComment extends BaseNode {
type: 'CSSComment';
value: string;
/** Character offset in a containing declaration value or at-rule prelude */
position?: number;
}
export interface StyleSheetFile extends StyleSheetBase {
@ -135,6 +144,7 @@ export namespace _CSS {
export interface PseudoElementSelector extends BaseNode {
type: 'PseudoElementSelector';
name: string;
args?: SelectorList;
}
export interface PseudoClassSelector extends BaseNode {

@ -18,10 +18,10 @@ export function css_props(element, get_styles) {
for (var key in styles) {
var value = styles[key];
if (value) {
style_set_property(/** @type {HTMLElement} */ (element), key, value);
} else {
if (value == null || value === '') {
style_remove_property(/** @type {HTMLElement} */ (element), key);
} else {
style_set_property(/** @type {HTMLElement} */ (element), key, value);
}
}
});

@ -111,8 +111,12 @@ function pause_effects(state, to_destroy, controlled_anchor) {
if (remaining === 0) {
// If we're in a controlled each block (i.e. the block is the only child of an
// element), and we are removing all items, _and_ there are no out transitions,
// we can use the fast path — emptying the element and replacing the anchor
var fast_path = transitions.length === 0 && controlled_anchor !== null;
// we can use the fast path — emptying the element and replacing the anchor.
// Skip the fast path when another batch is still pending on this each block:
// that batch's keys still reference EachItems in `state.items`, which
// `destroy_effects` needs to preserve offscreen (see #18610).
var fast_path =
transitions.length === 0 && controlled_anchor !== null && state.pending.size === 0;
if (fast_path) {
var anchor = /** @type {Element} */ (controlled_anchor);

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.56.8';
export const VERSION = '5.56.9';
export const PUBLIC_VERSION = '5';

@ -69,9 +69,21 @@ describe('parseCss', () => {
});
it('parses comments', () => {
const ast = parseCss('/* comment */ div { color: red; }');
const ast = parseCss(
'/* top */ div, /* selector */ span { /* block */ color: /* value */ red; }'
);
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Rule');
assert.deepEqual(
ast.comments.map((comment) => comment.value),
[' top ', ' selector ', ' block ', ' value ']
);
assert.deepEqual(ast.comments[0], {
type: 'CSSComment',
value: ' top ',
start: 0,
end: 9
});
});
it('parses complex selectors', () => {

@ -33,6 +33,7 @@
"type": "Style",
"start": 16,
"end": 56,
"comments": [],
"attributes": [],
"children": [
{

@ -33,6 +33,7 @@
"type": "Style",
"start": 16,
"end": 66,
"comments": [],
"attributes": [],
"children": [
{

@ -1071,6 +1071,14 @@
"end": 797
}
],
"comments": [
{
"type": "CSSComment",
"value": " test that all these are parsed correctly ",
"start": 12,
"end": 58
}
],
"content": {
"start": 7,
"end": 798,
@ -1125,5 +1133,6 @@
}
]
},
"options": null
"options": null,
"comments": []
}

@ -25,7 +25,35 @@
"type": "PseudoElementSelector",
"name": "view-transition-old",
"start": 60,
"end": 81
"end": 86,
"args": {
"type": "SelectorList",
"start": 82,
"end": 85,
"children": [
{
"type": "ComplexSelector",
"start": 82,
"end": 85,
"children": [
{
"type": "RelativeSelector",
"combinator": null,
"selectors": [
{
"type": "TypeSelector",
"name": "x-y",
"start": 82,
"end": 85
}
],
"start": 82,
"end": 85
}
]
}
]
}
}
],
"start": 60,
@ -89,7 +117,35 @@
"type": "PseudoElementSelector",
"name": "view-transition-old",
"start": 119,
"end": 140
"end": 145,
"args": {
"type": "SelectorList",
"start": 141,
"end": 144,
"children": [
{
"type": "ComplexSelector",
"start": 141,
"end": 144,
"children": [
{
"type": "RelativeSelector",
"combinator": null,
"selectors": [
{
"type": "TypeSelector",
"name": "x-y",
"start": 141,
"end": 144
}
],
"start": 141,
"end": 144
}
]
}
]
}
}
],
"start": 119,
@ -147,7 +203,35 @@
"type": "PseudoElementSelector",
"name": "highlight",
"start": 171,
"end": 182
"end": 199,
"args": {
"type": "SelectorList",
"start": 183,
"end": 198,
"children": [
{
"type": "ComplexSelector",
"start": 183,
"end": 198,
"children": [
{
"type": "RelativeSelector",
"combinator": null,
"selectors": [
{
"type": "TypeSelector",
"name": "rainbow-color-1",
"start": 183,
"end": 198
}
],
"start": 183,
"end": 198
}
]
}
]
}
}
],
"start": 171,
@ -200,7 +284,35 @@
"type": "PseudoElementSelector",
"name": "part",
"start": 234,
"end": 240
"end": 245,
"args": {
"type": "SelectorList",
"start": 241,
"end": 244,
"children": [
{
"type": "ComplexSelector",
"start": 241,
"end": 244,
"children": [
{
"type": "RelativeSelector",
"combinator": null,
"selectors": [
{
"type": "TypeSelector",
"name": "foo",
"start": 241,
"end": 244
}
],
"start": 241,
"end": 244
}
]
}
]
}
}
],
"start": 220,
@ -247,7 +359,35 @@
"type": "PseudoElementSelector",
"name": "slotted",
"start": 266,
"end": 275
"end": 285,
"args": {
"type": "SelectorList",
"start": 276,
"end": 284,
"children": [
{
"type": "ComplexSelector",
"start": 276,
"end": 284,
"children": [
{
"type": "RelativeSelector",
"combinator": null,
"selectors": [
{
"type": "ClassSelector",
"name": "content",
"start": 276,
"end": 284
}
],
"start": 276,
"end": 284
}
]
}
]
}
}
],
"start": 266,
@ -390,6 +530,26 @@
"end": 377
}
],
"comments": [
{
"type": "CSSComment",
"value": " test that all these are parsed correctly ",
"start": 12,
"end": 58
},
{
"type": "CSSComment",
"value": "button",
"start": 311,
"end": 321
},
{
"type": "CSSComment",
"value": "p after h1",
"start": 332,
"end": 346
}
],
"content": {
"start": 7,
"end": 378,
@ -405,5 +565,6 @@
"type": "Fragment",
"nodes": []
},
"options": null
"options": null,
"comments": []
}

@ -53,6 +53,7 @@
"end": 82
}
],
"comments": [],
"content": {
"start": 61,
"end": 83,
@ -77,6 +78,24 @@
]
},
"options": null,
"comments": [
{
"type": "Line",
"value": " script and style but no markup",
"start": 10,
"end": 43,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 34
}
}
}
],
"instance": {
"type": "Script",
"start": 0,

@ -68,6 +68,7 @@
"end": 196
}
],
"comments": [],
"content": {
"start": 43,
"end": 197,
@ -122,5 +123,6 @@
}
]
},
"options": null
"options": null,
"comments": []
}

@ -0,0 +1,7 @@
<Button
onclick={() => {
// belongs to onclick
run();
}}
onkeydown={() => run()}
/>

@ -0,0 +1,7 @@
<Button
onclick={() => {
// belongs to onclick
run();
}}
onkeydown={() => run()}
/>

@ -0,0 +1,5 @@
<button asdasd asdioqwjdoiqwjd qowdjqwoidjqowijdoiqj>
hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine
</button>
<span>hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine</span>

@ -0,0 +1,5 @@
<button asdasd asdioqwjdoiqwjd qowdjqwoidjqowijdoiqj>
hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine
</button>
<span>hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine</span>

@ -0,0 +1,19 @@
<pre>
hel
lo
.
</pre>
<pre> before
<span> nested
text </span>
{#if visible} conditional
value{/if}</pre>
<textarea> first
second </textarea>
<svelte:head><title> hello
world </title></svelte:head>

@ -0,0 +1,21 @@
<pre>
hel
lo
.
</pre>
<pre> before
<span> nested
text </span>
{#if visible} conditional
value{/if}</pre>
<textarea> first
second </textarea>
<svelte:head>
<title> hello
world </title>
</svelte:head>

@ -0,0 +1,26 @@
<style>
/* first */
/* second */
.foo, /* between */
.bar /* trailing */ {
/* block */
color: r /* value */ ed;
}
@media screen/* media */and (width > 0) {
/* nested */
.baz {
display: block;
}
}
.empty {
/* only */
}
.part::part(/* argument */ label) {
display: block;
}
/* last */
</style>

@ -0,0 +1,28 @@
<style>
/* first */
/* second */
.foo, /* between */
.bar /* trailing */ {
/* block */
color: r /* value */ ed;
}
@media screen/* media */and (width > 0) {
/* nested */
.baz {
display: block;
}
}
.empty {
/* only */
}
.part::part(/* argument */ label) {
display: block;
}
/* last */
</style>

@ -0,0 +1,50 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Regression for #18610: emptying a controlled keyed {#each} while another
// batch is still pending must not take the fast path that clears state.items
// before destroy_effects walks pending keys.
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>startA</button>
<button>startB</button>
<button>settleB</button>
<p>A0/B0</p>
<div><span>1</span><span>2</span></div>
`
);
const [startA, startB, settleB] = target.querySelectorAll('button');
// Batch A: add key 9, then block forever on gate A.
startA.click();
await tick();
// Batch B: empty the collection, then block on gate B.
startB.click();
await tick();
// Settle B first so B commits while A is still pending.
// Without the fix this throws reading `.e` of undefined and leaves a/b stuck.
settleB.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>startA</button>
<button>startB</button>
<button>settleB</button>
<p>A0/B1</p>
<div></div>
`
);
}
});

@ -0,0 +1,54 @@
<script>
let base = $state([1, 2]);
let extraKey = $state(/** @type {number | null} */ (null));
let tickA = $state(0);
let tickB = $state(0);
// Two independent sources so the batches touch disjoint source sets
// and are not merged.
const items = $derived(extraKey === null ? base : [...base, extraKey]);
/** @type {((value: string) => void) | undefined} */
let resolveB;
/**
* @param {string} name
* @param {number} n
*/
const gate = (name, n) =>
n === 0
? Promise.resolve(`${name}0`)
: new Promise((r) => {
if (name === 'B') resolveB = r;
});
const a = $derived(await gate('A', tickA));
const b = $derived(await gate('B', tickB));
function startA() {
extraKey = 9;
tickA = 1;
}
function startB() {
base = [];
tickB = 1;
}
function settleB() {
resolveB?.('B1');
}
</script>
<button onclick={startA}>startA</button>
<button onclick={startB}>startB</button>
<button onclick={settleB}>settleB</button>
<p>{a}/{b}</p>
<!-- Sole child so the each block is controlled and the fast path applies. -->
<div>
{#each items as item (item)}
<span>{item}</span>
{/each}
</div>

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>1</button><button>drop</button>`,
test({ assert, target }) {
const [bump, drop] = target.querySelectorAll('button');
bump?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>2</button><button>drop</button>`);
bump?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>3</button><button>drop</button>`);
drop?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>2</button><button>drop</button>`);
}
});

@ -0,0 +1,23 @@
<script>
class Counter {
#count = $state(1);
bump(other) {
other.#count++;
}
drop(other) {
--other.#count;
}
getCount() {
return this.#count;
}
}
const a = new Counter();
const b = new Counter();
</script>
<button onclick={() => a.bump(b)}>{b.getCount()}</button>
<button onclick={() => a.drop(b)}>drop</button>

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
ssrHtml: `<svelte-css-wrapper style="display: contents; --zero: 0; --one: 1;"><div>Hello</div></svelte-css-wrapper>`,
async test({ assert, target }) {
assert.htmlEqual(
target.innerHTML,
`<svelte-css-wrapper style="display: contents; --zero: 0; --one: 1;"><div>Hello</div></svelte-css-wrapper>`
);
}
});

@ -0,0 +1,5 @@
<script>
import Component from './Component.svelte';
</script>
<Component --zero={0} --one={1} --empty={''} --nullish={null} />

@ -1745,6 +1745,15 @@ declare module 'svelte/compiler' {
export interface StyleSheetBase extends BaseNode {
children: Array<Atrule | Rule>;
/** CSS comments in source order */
comments: CSSComment[];
}
export interface CSSComment extends BaseNode {
type: 'CSSComment';
value: string;
/** Character offset in a containing declaration value or at-rule prelude */
position?: number;
}
export interface StyleSheetFile extends StyleSheetBase {
@ -1839,6 +1848,7 @@ declare module 'svelte/compiler' {
export interface PseudoElementSelector extends BaseNode {
type: 'PseudoElementSelector';
name: string;
args?: SelectorList;
}
export interface PseudoClassSelector extends BaseNode {

Loading…
Cancel
Save