From d49b568019fbd6578ceba72de68d0790db909bee Mon Sep 17 00:00:00 2001 From: Snaipe Date: Mon, 27 Mar 2023 10:05:04 +0200 Subject: [PATCH 01/11] fix: allow use of document root as target in typescript (#7554) It is not possible to use typescript when using `target: document` during component initialization, because target can only be of type Element or ShadowRoot. This means that it is not possible to hydrate the entire document when managing the element as a Svelte component. This commit fixes this by allowing documents to be targets. --- src/runtime/internal/dev.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index cd47138fef..323312437b 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -164,8 +164,9 @@ export interface SvelteComponentDev { $destroy(): void; [accessor: string]: any; } + export interface ComponentConstructorOptions = Record> { - target: Element | ShadowRoot; + target: Element | Document | ShadowRoot; anchor?: Element; props?: Props; context?: Map; From 91e8dfcd6d4be0e5b0f3eea46b0ab21023d8fb60 Mon Sep 17 00:00:00 2001 From: Michael Leahy Date: Mon, 27 Mar 2023 01:07:39 -0700 Subject: [PATCH 02/11] feat: `container` query support via `css-tree` extension (#8275) Closes #6969 As discussed there, container query support is quite useful to add to Svelte as it is now broadly available with Firefox releasing support imminently w/ FF v110 this upcoming week (~Feb 14th). Chrome has had support since ~Aug '22. The central issue is that css-tree which is a dependency for CSS AST parsing is significantly lagging behind on adding more recent features such as container query support. Ample time has been given to the maintainer to update css-tree and I do have every confidence that in time css-tree will receive a new major version with all sorts of modern CSS syntax supported including container queries. This PR provides an interim solution for what Svelte needs to support container queries now. --- src/compiler/compile/css/Stylesheet.ts | 2 +- .../parse/read/css-tree-cq/css_tree_parse.ts | 43 ++++++ .../parse/read/css-tree-cq/node/comparison.ts | 48 +++++++ .../css-tree-cq/node/container_feature.ts | 82 +++++++++++ .../node/container_feature_range.ts | 86 ++++++++++++ .../node/container_feature_style.ts | 85 ++++++++++++ .../read/css-tree-cq/node/container_query.ts | 130 ++++++++++++++++++ .../css-tree-cq/node/query_css_function.ts | 41 ++++++ src/compiler/parse/read/style.ts | 5 +- test/css/samples/container-query/expected.css | 1 + test/css/samples/container-query/input.svelte | 87 ++++++++++++ 11 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 src/compiler/parse/read/css-tree-cq/css_tree_parse.ts create mode 100644 src/compiler/parse/read/css-tree-cq/node/comparison.ts create mode 100644 src/compiler/parse/read/css-tree-cq/node/container_feature.ts create mode 100644 src/compiler/parse/read/css-tree-cq/node/container_feature_range.ts create mode 100644 src/compiler/parse/read/css-tree-cq/node/container_feature_style.ts create mode 100644 src/compiler/parse/read/css-tree-cq/node/container_query.ts create mode 100644 src/compiler/parse/read/css-tree-cq/node/query_css_function.ts create mode 100644 test/css/samples/container-query/expected.css create mode 100644 test/css/samples/container-query/input.svelte diff --git a/src/compiler/compile/css/Stylesheet.ts b/src/compiler/compile/css/Stylesheet.ts index 8f54830e6a..7cb1af3635 100644 --- a/src/compiler/compile/css/Stylesheet.ts +++ b/src/compiler/compile/css/Stylesheet.ts @@ -173,7 +173,7 @@ class Atrule { } apply(node: Element) { - if (this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') { + if (this.node.name === 'container' || this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') { this.children.forEach(child => { child.apply(node); }); diff --git a/src/compiler/parse/read/css-tree-cq/css_tree_parse.ts b/src/compiler/parse/read/css-tree-cq/css_tree_parse.ts new file mode 100644 index 0000000000..d56e6d72ed --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/css_tree_parse.ts @@ -0,0 +1,43 @@ +// @ts-nocheck +// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from +// `css-tree` Node module directly. This allows the production build of Svelte to work correctly. +import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js'; + +import * as Comparison from './node/comparison'; +import * as ContainerFeature from './node/container_feature'; +import * as ContainerFeatureRange from './node/container_feature_range'; +import * as ContainerFeatureStyle from './node/container_feature_style'; +import * as ContainerQuery from './node/container_query'; +import * as QueryCSSFunction from './node/query_css_function'; + +/** + * Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`. + * + * The new nodes are located in `./node`. + */ +const cqSyntax = fork({ + atrule: { // extend or override at-rule dictionary + container: { + parse: { + prelude() { + return this.createSingleNodeList( + this.ContainerQuery() + ); + }, + block(isStyleBlock = false) { + return this.Block(isStyleBlock); + } + } + } + }, + node: { // extend node types + Comparison, + ContainerFeature, + ContainerFeatureRange, + ContainerFeatureStyle, + ContainerQuery, + QueryCSSFunction + } +}); + +export const parse = cqSyntax.parse; diff --git a/src/compiler/parse/read/css-tree-cq/node/comparison.ts b/src/compiler/parse/read/css-tree-cq/node/comparison.ts new file mode 100644 index 0000000000..4f40f59c24 --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/node/comparison.ts @@ -0,0 +1,48 @@ +// @ts-nocheck +import { Delim } from 'css-tree/tokenizer'; + +export const name = 'Comparison'; +export const structure = { + value: String +}; + +export function parse() { + const start = this.tokenStart; + + const char1 = this.consume(Delim); + + // The first character in the comparison operator must match '<', '=', or '>'. + if (char1 !== '<' && char1 !== '>' && char1 !== '=') { + this.error('Malformed comparison operator'); + } + + let char2; + + if (this.tokenType === Delim) { + char2 = this.consume(Delim); + + // The second character in the comparison operator must match '='. + if (char2 !== '=') { + this.error('Malformed comparison operator'); + } + } + + // If the next token is also 'Delim' then it is malformed. + if (this.tokenType === Delim) { + this.error('Malformed comparison operator'); + } + + const value = char2 ? `${char1}${char2}` : char1; + + return { + type: 'Comparison', + loc: this.getLocation(start, this.tokenStart), + value + }; +} + +export function generate(node) { + for (let index = 0; index < node.value.length; index++) { + this.token(Delim, node.value.charAt(index)); + } +} diff --git a/src/compiler/parse/read/css-tree-cq/node/container_feature.ts b/src/compiler/parse/read/css-tree-cq/node/container_feature.ts new file mode 100644 index 0000000000..d84d5f5afd --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/node/container_feature.ts @@ -0,0 +1,82 @@ +// @ts-nocheck +import { + Ident, + Number, + Dimension, + Function, + LeftParenthesis, + RightParenthesis, + Colon, + Delim +} from 'css-tree/tokenizer'; + +export const name = 'ContainerFeature'; +export const structure = { + name: String, + value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null] +}; + +export function parse() { + const start = this.tokenStart; + let value = null; + + this.eat(LeftParenthesis); + this.skipSC(); + + const name = this.consume(Ident); + this.skipSC(); + + if (this.tokenType !== RightParenthesis) { + this.eat(Colon); + this.skipSC(); + + switch (this.tokenType) { + case Number: + if (this.lookupNonWSType(1) === Delim) { + value = this.Ratio(); + } else { + value = this.Number(); + } + break; + + case Dimension: + value = this.Dimension(); + break; + + case Function: + value = this.QueryCSSFunction(); + break; + + case Ident: + value = this.Identifier(); + break; + + default: + this.error('Number, dimension, ratio, function, or identifier is expected'); + break; + } + + this.skipSC(); + } + + this.eat(RightParenthesis); + + return { + type: 'ContainerFeature', + loc: this.getLocation(start, this.tokenStart), + name, + value + }; +} + +export function generate(node) { + this.token(LeftParenthesis, '('); + this.token(Ident, node.name); + + if (node.value !== null) { + this.token(Colon, ':'); + this.node(node.value); + } + + this.token(RightParenthesis, ')'); +} diff --git a/src/compiler/parse/read/css-tree-cq/node/container_feature_range.ts b/src/compiler/parse/read/css-tree-cq/node/container_feature_range.ts new file mode 100644 index 0000000000..747fbe8da0 --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/node/container_feature_range.ts @@ -0,0 +1,86 @@ +// @ts-nocheck +import { + Ident, + Number, + Delim, + Dimension, + Function, + LeftParenthesis, + RightParenthesis, + WhiteSpace +} from 'css-tree/tokenizer'; + +export const name = 'ContainerFeatureRange'; +export const structure = { + name: String, + value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null] +}; + +function lookup_non_WS_type_and_value(offset, type, referenceStr) { + let current_type; + + do { + current_type = this.lookupType(offset++); + if (current_type !== WhiteSpace) { + break; + } + } while (current_type !== 0); // NULL -> 0 + + return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false; +} + +export function parse() { + const children = this.createList(); + let child = null; + + this.eat(LeftParenthesis); + this.skipSC(); + + while (!this.eof && this.tokenType !== RightParenthesis) { + switch (this.tokenType) { + case Number: + if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) { + child = this.Ratio(); + } else { + child = this.Number(); + } + break; + + case Delim: + child = this.Comparison(); + break; + + case Dimension: + child = this.Dimension(); + break; + + case Function: + child = this.QueryCSSFunction(); + break; + + case Ident: + child = this.Identifier(); + break; + + default: + this.error('Number, dimension, comparison, ratio, function, or identifier is expected'); + break; + } + + children.push(child); + + this.skipSC(); + } + + this.eat(RightParenthesis); + + return { + type: 'ContainerFeatureRange', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node); +} diff --git a/src/compiler/parse/read/css-tree-cq/node/container_feature_style.ts b/src/compiler/parse/read/css-tree-cq/node/container_feature_style.ts new file mode 100644 index 0000000000..343b5d47ca --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/node/container_feature_style.ts @@ -0,0 +1,85 @@ +// @ts-nocheck +import { + Function, + Ident, + Number, + Dimension, + RightParenthesis, + Colon, + Delim +} from 'css-tree/tokenizer'; + +export const name = 'ContainerFeatureStyle'; +export const structure = { + name: String, + value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null] +}; + +export function parse() { + const start = this.tokenStart; + let value = null; + + const function_name = this.consumeFunctionName(); + if (function_name !== 'style') { + this.error('Unknown container style query identifier; "style" is expected'); + } + + this.skipSC(); + + const name = this.consume(Ident); + this.skipSC(); + + if (this.tokenType !== RightParenthesis) { + this.eat(Colon); + this.skipSC(); + + switch (this.tokenType) { + case Number: + if (this.lookupNonWSType(1) === Delim) { + value = this.Ratio(); + } else { + value = this.Number(); + } + break; + + case Dimension: + value = this.Dimension(); + break; + + case Function: + value = this.QueryCSSFunction(); + break; + + case Ident: + value = this.Identifier(); + break; + + default: + this.error('Number, dimension, ratio, function or identifier is expected'); + break; + } + + this.skipSC(); + } + + this.eat(RightParenthesis); + + return { + type: 'ContainerFeatureStyle', + loc: this.getLocation(start, this.tokenStart), + name, + value + }; +} + +export function generate(node) { + this.token(Function, 'style('); + this.token(Ident, node.name); + + if (node.value !== null) { + this.token(Colon, ':'); + this.node(node.value); + } + + this.token(RightParenthesis, ')'); +} diff --git a/src/compiler/parse/read/css-tree-cq/node/container_query.ts b/src/compiler/parse/read/css-tree-cq/node/container_query.ts new file mode 100644 index 0000000000..c88b91f12d --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/node/container_query.ts @@ -0,0 +1,130 @@ +// @ts-nocheck +import { + EOF, + WhiteSpace, + Comment, + Delim, + Function, + Ident, + LeftParenthesis, + RightParenthesis, + LeftCurlyBracket, + Colon +} from 'css-tree/tokenizer'; + +const CONTAINER_QUERY_KEYWORDS = new Set(['none', 'and', 'not', 'or']); + +export const name = 'ContainerQuery'; +export const structure = { + name: 'Identifier', + children: [[ + 'Identifier', + 'ContainerFeature', + 'ContainerFeatureRange', + 'ContainerFeatureStyle', + 'WhiteSpace' + ]] +}; + +/** + * Looks ahead to determine if query feature is a range query. This involves locating at least one delimiter and no + * colon tokens. + * + * @returns {boolean} Is potential range query. + */ +function lookahead_is_range() { + let type; + let offset = 0; + + let count = 0; + let delim_found = false; + let no_colon = true; + + // A range query has maximum 5 tokens when formatted as 'mf-range' / + // ' '. So only look ahead maximum of 6 non-whitespace tokens. + do { + type = this.lookupNonWSType(offset++); + if (type !== WhiteSpace) { + count++; + } + if (type === Delim) { + delim_found = true; + } + if (type === Colon) { + no_colon = false; + } + if (type === LeftCurlyBracket || type === RightParenthesis) { + break; + } + } while (type !== EOF && count <= 6); + + return delim_found && no_colon; +} + +export function parse() { + const start = this.tokenStart; + const children = this.createList(); + let child = null; + let name = null; + + // Parse potential container name. + if (this.tokenType === Ident) { + const container_name = this.substring(this.tokenStart, this.tokenEnd); + + // Container name doesn't match a query keyword, so assign it as container name. + if (!CONTAINER_QUERY_KEYWORDS.has(container_name.toLowerCase())) { + name = container_name; + this.eatIdent(container_name); + } + } + + this.skipSC(); + + scan: + while (!this.eof) { + switch (this.tokenType) { + case Comment: + case WhiteSpace: + this.next(); + continue; + + case Ident: + child = this.Identifier(); + break; + + case Function: + child = this.ContainerFeatureStyle(); + break; + + case LeftParenthesis: + // Lookahead to determine if range feature. + child = lookahead_is_range.call(this) ? this.ContainerFeatureRange() : this.ContainerFeature(); + break; + + default: + break scan; + } + + children.push(child); + } + + if (child === null) { + this.error('Identifier or parenthesis is expected'); + } + + return { + type: 'ContainerQuery', + loc: this.getLocation(start, this.tokenStart - 1), + name, + children + }; +} + +export function generate(node) { + if (typeof node.name === 'string') { + this.token(Ident, node.name); + } + + this.children(node); +} + diff --git a/src/compiler/parse/read/css-tree-cq/node/query_css_function.ts b/src/compiler/parse/read/css-tree-cq/node/query_css_function.ts new file mode 100644 index 0000000000..792aeabd35 --- /dev/null +++ b/src/compiler/parse/read/css-tree-cq/node/query_css_function.ts @@ -0,0 +1,41 @@ +// @ts-nocheck +import { + RightParenthesis +} from 'css-tree/tokenizer'; + +const QUERY_CSS_FUNCTIONS = new Set(['calc', 'clamp', 'min', 'max']); + +export const name = 'QueryCSSFunction'; +export const structure = { + name: String, + expression: String +}; + +export function parse() { + const start = this.tokenStart; + + const name = this.consumeFunctionName(); + + if (!QUERY_CSS_FUNCTIONS.has(name)) { + this.error('Unknown query single value function; expected: "calc", "clamp", "max", min"'); + } + + const body = this.Raw(this.tokenIndex, null, false); + + this.eat(RightParenthesis); + + return { + type: 'QueryCSSFunction', + loc: this.getLocation(start, this.tokenStart), + name, + expression: body.value + }; +} + +export function generate(node) { + this.token(Function, `${node.name}(`); + + this.node(node.expression); + + this.token(RightParenthesis, ')'); +} diff --git a/src/compiler/parse/read/style.ts b/src/compiler/parse/read/style.ts index 3ed510c3d0..eba86d22de 100644 --- a/src/compiler/parse/read/style.ts +++ b/src/compiler/parse/read/style.ts @@ -1,5 +1,6 @@ // @ts-ignore -import parse from 'css-tree/parser'; +// import parse from 'css-tree/parser'; // When css-tree supports container queries uncomment. +import { parse } from './css-tree-cq/css_tree_parse'; // Use extended css-tree for container query support. import { walk } from 'estree-walker'; import { Parser } from '../index'; import { Node } from 'estree'; @@ -78,7 +79,7 @@ export default function read_style(parser: Parser, start: number, attributes: No }); parser.read(regex_starts_with_closing_style_tag); - + const end = parser.index; return { diff --git a/test/css/samples/container-query/expected.css b/test/css/samples/container-query/expected.css new file mode 100644 index 0000000000..5002ed9c83 --- /dev/null +++ b/test/css/samples/container-query/expected.css @@ -0,0 +1 @@ +div.svelte-xyz{container:test-container / inline-size}@container (min-width: 400px){div.svelte-xyz{color:red}}@container test-container (min-width: 410px){div.svelte-xyz{color:green}}@container test-container (width < 400px){div.svelte-xyz{color:blue}}@container test-container (0 <= width < 300px){div.svelte-xyz{color:purple}}@container not (width < 400px){div.svelte-xyz{color:pink}}@container (width > 400px) and (height > 400px){div.svelte-xyz{color:lightgreen}}@container (width > 400px) or (height > 400px){div.svelte-xyz{color:lightblue}}@container (width > 400px) and (width > 800px) or (orientation: portrait){div.svelte-xyz{color:salmon}}@container style(color: blue){div.svelte-xyz{color:tan}}@container test-container (min-width: calc(400px + 1px)){div.svelte-xyz{color:green}}@container test-container (width < clamp(200px, 40%, 400px)){div.svelte-xyz{color:blue}}@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)){div.svelte-xyz{color:purple}}@container style(--var: calc(400px + 1px)){div.svelte-xyz{color:sandybrown}} \ No newline at end of file diff --git a/test/css/samples/container-query/input.svelte b/test/css/samples/container-query/input.svelte new file mode 100644 index 0000000000..bb6de73618 --- /dev/null +++ b/test/css/samples/container-query/input.svelte @@ -0,0 +1,87 @@ +
container query
+ + From 95c46552fe076fa33c2ef38072b4604a98708e60 Mon Sep 17 00:00:00 2001 From: esthe <57283066+esthedebeste@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:12:44 +0200 Subject: [PATCH 03/11] feat: make `preserveComments` effective in DOM renderer (#7182) --- .../compile/render_dom/wrappers/Comment.ts | 41 +++++++++++++ .../compile/render_dom/wrappers/Fragment.ts | 5 +- src/runtime/internal/dom.ts | 17 ++++++ .../samples/claim-comment/_after.html | 1 + .../samples/claim-comment/_before.html | 1 + .../samples/claim-comment/_config.js | 20 +++++++ .../samples/claim-comment/main.svelte | 1 + .../samples/dom-preserve-comments/_config.js | 5 ++ .../samples/dom-preserve-comments/expected.js | 58 +++++++++++++++++++ .../dom-preserve-comments/input.svelte | 3 + 10 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/compiler/compile/render_dom/wrappers/Comment.ts create mode 100644 test/hydration/samples/claim-comment/_after.html create mode 100644 test/hydration/samples/claim-comment/_before.html create mode 100644 test/hydration/samples/claim-comment/_config.js create mode 100644 test/hydration/samples/claim-comment/main.svelte create mode 100644 test/js/samples/dom-preserve-comments/_config.js create mode 100644 test/js/samples/dom-preserve-comments/expected.js create mode 100644 test/js/samples/dom-preserve-comments/input.svelte diff --git a/src/compiler/compile/render_dom/wrappers/Comment.ts b/src/compiler/compile/render_dom/wrappers/Comment.ts new file mode 100644 index 0000000000..a8c63b4227 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/Comment.ts @@ -0,0 +1,41 @@ +import Renderer from '../Renderer'; +import Block from '../Block'; +import Comment from '../../nodes/Comment'; +import Wrapper from './shared/Wrapper'; +import { x } from 'code-red'; +import { Identifier } from 'estree'; + +export default class CommentWrapper extends Wrapper { + node: Comment; + var: Identifier; + + constructor( + renderer: Renderer, + block: Block, + parent: Wrapper, + node: Comment + ) { + super(renderer, block, parent, node); + this.var = x`c` as Identifier; + } + + render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { + if (!this.renderer.options.preserveComments) return; + + const string_literal = { + type: 'Literal', + value: this.node.data, + loc: { + start: this.renderer.locate(this.node.start), + end: this.renderer.locate(this.node.end) + } + }; + + block.add_element( + this.var, + x`@comment(${string_literal})`, + parent_nodes && x`@claim_comment(${parent_nodes}, ${string_literal})`, + parent_node + ); + } +} diff --git a/src/compiler/compile/render_dom/wrappers/Fragment.ts b/src/compiler/compile/render_dom/wrappers/Fragment.ts index 463c8db24a..736cd4e1ef 100644 --- a/src/compiler/compile/render_dom/wrappers/Fragment.ts +++ b/src/compiler/compile/render_dom/wrappers/Fragment.ts @@ -14,6 +14,7 @@ import RawMustacheTag from './RawMustacheTag'; import Slot from './Slot'; import SlotTemplate from './SlotTemplate'; import Text from './Text'; +import Comment from './Comment'; import Title from './Title'; import Window from './Window'; import { INode } from '../../nodes/interfaces'; @@ -27,7 +28,7 @@ import { regex_starts_with_whitespace } from '../../../utils/patterns'; const wrappers = { AwaitBlock, Body, - Comment: null, + Comment, DebugTag, Document, EachBlock, @@ -118,7 +119,7 @@ export default class FragmentWrapper { link(last_child, last_child = wrapper); } else { const Wrapper = wrappers[child.type]; - if (!Wrapper) continue; + if (!Wrapper || (child.type === 'Comment' && !renderer.options.preserveComments)) continue; const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling); this.nodes.unshift(wrapper); diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index a746613170..34049b3580 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -254,6 +254,10 @@ export function empty() { return text(''); } +export function comment(content: string) { + return document.createComment(content); +} + export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) { node.addEventListener(event, handler, options); return () => node.removeEventListener(event, handler, options); @@ -550,6 +554,19 @@ export function claim_space(nodes) { return claim_text(nodes, ' '); } +export function claim_comment(nodes:ChildNodeArray, data) { + return claim_node( + nodes, + (node: ChildNode): node is Comment => node.nodeType === 8, + (node: Comment) => { + node.data = '' + data; + return undefined; + }, + () => comment(data), + true + ); +} + function find_comment(nodes, text, start) { for (let i = start; i < nodes.length; i += 1) { const node = nodes[i]; diff --git a/test/hydration/samples/claim-comment/_after.html b/test/hydration/samples/claim-comment/_after.html new file mode 100644 index 0000000000..3029a9dd64 --- /dev/null +++ b/test/hydration/samples/claim-comment/_after.html @@ -0,0 +1 @@ +
diff --git a/test/hydration/samples/claim-comment/_before.html b/test/hydration/samples/claim-comment/_before.html new file mode 100644 index 0000000000..736fb4602d --- /dev/null +++ b/test/hydration/samples/claim-comment/_before.html @@ -0,0 +1 @@ +
diff --git a/test/hydration/samples/claim-comment/_config.js b/test/hydration/samples/claim-comment/_config.js new file mode 100644 index 0000000000..0f69bd48f6 --- /dev/null +++ b/test/hydration/samples/claim-comment/_config.js @@ -0,0 +1,20 @@ +export default { + compileOptions: { + preserveComments:true + }, + snapshot(target) { + const div = target.querySelector('div'); + + return { + div, + comment: div.childNodes[0] + }; + }, + + test(assert, target, snapshot) { + const div = target.querySelector('div'); + assert.equal(div, snapshot.div); + assert.equal(div.childNodes[0], snapshot.comment); + assert.equal(div.childNodes[1].nodeType, 8); + } +}; diff --git a/test/hydration/samples/claim-comment/main.svelte b/test/hydration/samples/claim-comment/main.svelte new file mode 100644 index 0000000000..3029a9dd64 --- /dev/null +++ b/test/hydration/samples/claim-comment/main.svelte @@ -0,0 +1 @@ +
diff --git a/test/js/samples/dom-preserve-comments/_config.js b/test/js/samples/dom-preserve-comments/_config.js new file mode 100644 index 0000000000..76af4bb39a --- /dev/null +++ b/test/js/samples/dom-preserve-comments/_config.js @@ -0,0 +1,5 @@ +export default { + options: { + preserveComments: true + } +}; diff --git a/test/js/samples/dom-preserve-comments/expected.js b/test/js/samples/dom-preserve-comments/expected.js new file mode 100644 index 0000000000..aa9f509263 --- /dev/null +++ b/test/js/samples/dom-preserve-comments/expected.js @@ -0,0 +1,58 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + comment, + detach, + element, + init, + insert, + noop, + safe_not_equal, + space +} from "svelte/internal"; + +function create_fragment(ctx) { + let div0; + let t1; + let c; + let t2; + let div1; + + return { + c() { + div0 = element("div"); + div0.textContent = "content"; + t1 = space(); + c = comment(" comment "); + t2 = space(); + div1 = element("div"); + div1.textContent = "more content"; + }, + m(target, anchor) { + insert(target, div0, anchor); + insert(target, t1, anchor); + insert(target, c, anchor); + insert(target, t2, anchor); + insert(target, div1, anchor); + }, + p: noop, + i: noop, + o: noop, + d(detaching) { + if (detaching) detach(div0); + if (detaching) detach(t1); + if (detaching) detach(c); + if (detaching) detach(t2); + if (detaching) detach(div1); + } + }; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, null, create_fragment, safe_not_equal, {}); + } +} + +export default Component; \ No newline at end of file diff --git a/test/js/samples/dom-preserve-comments/input.svelte b/test/js/samples/dom-preserve-comments/input.svelte new file mode 100644 index 0000000000..7f7e291caa --- /dev/null +++ b/test/js/samples/dom-preserve-comments/input.svelte @@ -0,0 +1,3 @@ +
content
+ +
more content
From dadd6fe9455f0f2e7a7d1dce365e50d64953bc3e Mon Sep 17 00:00:00 2001 From: Nguyen Tran <88808276+ngtr6788@users.noreply.github.com> Date: Mon, 27 Mar 2023 04:15:39 -0400 Subject: [PATCH 04/11] fix: resolve computed_prop_# collision (#8418) Fixes #8417 The issue is that unpack_destructuring in each blocks, await blocks, and @const tags were making computed_props independently. This causes computed_props_# to conflict when each blocks were used with @const tags, or await blocks and @const tags, or consecutive @const tags together. Therefore, one solution is to use component.get_unique_name to, well, make unique names and never get conflicts. --- src/compiler/compile/nodes/shared/Context.ts | 30 ++++--------- .../_config.js | 20 +++++++++ .../main.svelte | 27 ++++++++++++ .../_config.js | 15 +++++++ .../main.svelte | 44 +++++++++++++++++++ 5 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/_config.js create mode 100644 test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/main.svelte create mode 100644 test/runtime/samples/const-tag-each-destructure-computed-in-computed/_config.js create mode 100644 test/runtime/samples/const-tag-each-destructure-computed-in-computed/main.svelte diff --git a/src/compiler/compile/nodes/shared/Context.ts b/src/compiler/compile/nodes/shared/Context.ts index 3957ed2173..76ac895681 100644 --- a/src/compiler/compile/nodes/shared/Context.ts +++ b/src/compiler/compile/nodes/shared/Context.ts @@ -11,7 +11,7 @@ export type Context = DestructuredVariable | ComputedProperty; interface ComputedProperty { type: 'ComputedProperty'; - property_name: string; + property_name: Identifier; key: Expression | PrivateIdentifier; } @@ -30,8 +30,7 @@ export function unpack_destructuring({ default_modifier = (node) => node, scope, component, - context_rest_properties, - number_of_computed_props = { n: 0 } + context_rest_properties }: { contexts: Context[]; node: Node; @@ -40,10 +39,6 @@ export function unpack_destructuring({ scope: TemplateScope; component: Component; context_rest_properties: Map; - // we want to pass this by reference, as a sort of global variable, because - // if we pass this by value, we could get computed_property_# variable collisions - // when we deal with nested object destructuring - number_of_computed_props?: { n: number }; }) { if (!node) return; @@ -72,8 +67,7 @@ export function unpack_destructuring({ default_modifier, scope, component, - context_rest_properties, - number_of_computed_props + context_rest_properties }); context_rest_properties.set((element.argument as Identifier).name, element); } else if (element && element.type === 'AssignmentPattern') { @@ -93,8 +87,7 @@ export function unpack_destructuring({ )}` as Node, scope, component, - context_rest_properties, - number_of_computed_props + context_rest_properties }); } else { unpack_destructuring({ @@ -104,8 +97,7 @@ export function unpack_destructuring({ default_modifier, scope, component, - context_rest_properties, - number_of_computed_props + context_rest_properties }); } }); @@ -124,8 +116,7 @@ export function unpack_destructuring({ default_modifier, scope, component, - context_rest_properties, - number_of_computed_props + context_rest_properties }); context_rest_properties.set((property.argument as Identifier).name, property); } else if (property.type === 'Property') { @@ -136,8 +127,7 @@ export function unpack_destructuring({ if (property.computed) { // e.g { [computedProperty]: ... } - const property_name = `computed_property_${number_of_computed_props.n}`; - number_of_computed_props.n += 1; + const property_name = component.get_unique_name('computed_property'); contexts.push({ type: 'ComputedProperty', @@ -178,8 +168,7 @@ export function unpack_destructuring({ )}` as Node, scope, component, - context_rest_properties, - number_of_computed_props + context_rest_properties }); } else { // e.g. { property } or { property: newName } @@ -190,8 +179,7 @@ export function unpack_destructuring({ default_modifier, scope, component, - context_rest_properties, - number_of_computed_props + context_rest_properties }); } } diff --git a/test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/_config.js b/test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/_config.js new file mode 100644 index 0000000000..9ec4a3e3e6 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/_config.js @@ -0,0 +1,20 @@ +export default { + html: ` +

4, 12, 60

+ `, + + async test({ component, target, assert }) { + component.permutation = [2, 3, 1]; + await (component.promise1 = Promise.resolve({length: 1, width: 2, height: 3})); + try { + await (component.promise2 = Promise.reject({length: 97, width: 98, height: 99})); + } catch (e) { + // nothing + } + + assert.htmlEqual(target.innerHTML, ` +

2, 11, 2

+

9506, 28811, 98

+ `); + } +}; diff --git a/test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/main.svelte b/test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/main.svelte new file mode 100644 index 0000000000..285fc44348 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then-destructuring-computed-in-computed/main.svelte @@ -0,0 +1,27 @@ + + +{#await promise1 then { length, width, height }} + {@const { [0]: a, [1]: b, [2]: c } = permutation} + {@const { [`${a}-Dimensions`]: { [c - 1]: first }, [`${b}-Dimensions`]: { [b - 1]: second }, [`${c}-Dimensions`]: { [a - 1]: third } } = calculate(length, width, height) } +

{first}, {second}, {third}

+{/await} + +{#await promise2 catch { [`leng${th}`]: l, [`wid${th}`]: w, height: h }} + {@const [a, b, c] = permutation} + {@const { [`${a}-Dimensions`]: { [c - 1]: first }, [`${b}-Dimensions`]: { [b - 1]: second }, [`${c}-Dimensions`]: { [a - 1]: third } } = calculate(l, w, h) } +

{first}, {second}, {third}

+{/await} diff --git a/test/runtime/samples/const-tag-each-destructure-computed-in-computed/_config.js b/test/runtime/samples/const-tag-each-destructure-computed-in-computed/_config.js new file mode 100644 index 0000000000..c299f19da6 --- /dev/null +++ b/test/runtime/samples/const-tag-each-destructure-computed-in-computed/_config.js @@ -0,0 +1,15 @@ +export default { + html: ` + + + + `, + + async test({ component, target, assert }) { + component.boxes = [{ length: 10, width: 20, height: 30 }]; + + assert.htmlEqual(target.innerHTML, + '' + ); + } +}; diff --git a/test/runtime/samples/const-tag-each-destructure-computed-in-computed/main.svelte b/test/runtime/samples/const-tag-each-destructure-computed-in-computed/main.svelte new file mode 100644 index 0000000000..26dc5557ed --- /dev/null +++ b/test/runtime/samples/const-tag-each-destructure-computed-in-computed/main.svelte @@ -0,0 +1,44 @@ + + +{#each boxes as { [`leng${th}`]: length, [`wid${th}`]: width, height }} + {@const { + [`two${dimension}`]: areas, + [`three${dimension}`]: { + volume + } + } = calculate(length, width, height)} + {@const { + i = 1, + [`bottom${area}`]: bottom, + [`side${area}${i++}`]: sideone, + [`side${area}${i++}`]: sidetwo + } = areas} + +{/each} From b8959ac09ec78a0c0aad3d1d846d2eda569bcbba Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:32:41 +0200 Subject: [PATCH 05/11] chore: update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e886996a80..0c375e75ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ * Relax `a11y-no-noninteractive-element-to-interactive-role` warning ([#8402](https://github.com/sveltejs/svelte/pull/8402)) * Add `a11y-interactive-supports-focus` warning ([#8392](https://github.com/sveltejs/svelte/pull/8392)) * Fix equality check when updating dynamic text ([#5931](https://github.com/sveltejs/svelte/issues/5931)) +* Make `preserveComments` effective in DOM renderer ([#7182](https://github.com/sveltejs/svelte/pull/7182)) +* Add CSS container queries support ([#6969](https://github.com/sveltejs/svelte/issues/6969)) +* Properly handle microdata attributes ([#8414](https://github.com/sveltejs/svelte/pull/8414)) +* Prevent name collision when using computed destructuring variables ([#8417](https://github.com/sveltejs/svelte/issues/8417)) +* Allow use of `document` for `target` in typings ([#7554](https://github.com/sveltejs/svelte/pull/7554)) ## 3.57.0 From 1333be0c6ad22fc49792a76478fcb231314bb765 Mon Sep 17 00:00:00 2001 From: Filip Ambrosius Date: Mon, 27 Mar 2023 11:02:12 +0200 Subject: [PATCH 06/11] fix: Regression `itemscope` as `boolean_attribute` (#8414) Microdata are a strange set of attributes which are ONLY defined in markup, and have no relationship to the underlying Document Object Model node. As such programmatically defining an element and setting a property on it with a given Microdata attribute will not work: https://codepen.io/iambrosius/full/jOvXBBG One can read more about microdata here: https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata The fix is to remove itemscope being a boolean attribute, because that opts into a transformation as a DOM property, which is wrong. --- .../render_dom/wrappers/Element/Attribute.ts | 1 - src/shared/boolean_attributes.ts | 1 - .../attribute-boolean-hidden/_config.js | 10 ++++++ .../attribute-boolean-hidden/main.svelte | 5 +++ .../attribute-boolean-itemscope/_config.js | 11 ------- .../attribute-boolean-itemscope/main.svelte | 5 --- .../samples/attribute-microdata/_config.js | 25 +++++++++++++++ .../samples/attribute-microdata/main.svelte | 31 +++++++++++++++++++ 8 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 test/runtime/samples/attribute-boolean-hidden/_config.js create mode 100644 test/runtime/samples/attribute-boolean-hidden/main.svelte delete mode 100644 test/runtime/samples/attribute-boolean-itemscope/_config.js delete mode 100644 test/runtime/samples/attribute-boolean-itemscope/main.svelte create mode 100644 test/runtime/samples/attribute-microdata/_config.js create mode 100644 test/runtime/samples/attribute-microdata/main.svelte diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index 6cb3a00218..64178030f6 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -364,7 +364,6 @@ const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & { [k indeterminate: { applies_to: ['input'] }, inert: {}, ismap: { property_name: 'isMap', applies_to: ['img'] }, - itemscope: {}, loop: { applies_to: ['audio', 'bgsound', 'video'] }, multiple: { applies_to: ['input', 'select'] }, muted: { applies_to: ['audio', 'video'] }, diff --git a/src/shared/boolean_attributes.ts b/src/shared/boolean_attributes.ts index 668dd97975..10513ec712 100644 --- a/src/shared/boolean_attributes.ts +++ b/src/shared/boolean_attributes.ts @@ -13,7 +13,6 @@ const _boolean_attributes = [ 'hidden', 'inert', 'ismap', - 'itemscope', 'loop', 'multiple', 'muted', diff --git a/test/runtime/samples/attribute-boolean-hidden/_config.js b/test/runtime/samples/attribute-boolean-hidden/_config.js new file mode 100644 index 0000000000..cb38feb93d --- /dev/null +++ b/test/runtime/samples/attribute-boolean-hidden/_config.js @@ -0,0 +1,10 @@ +export default { + props: { + hidden: true + }, + html: '