feat: implement `:global {...}` CSS blocks (#11276)

* feat: implement `:global {...}` CSS blocks

* tests for compiler errors

* regenerate types

* lint
pull/11260/head
Rich Harris 1 year ago committed by GitHub
parent 11c7cd5495
commit 9721d5641b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: implement `:global {...}` CSS blocks

@ -103,6 +103,15 @@ const css = {
/** @param {string} message */ /** @param {string} message */
'css-parse-error': (message) => message, 'css-parse-error': (message) => message,
'invalid-css-empty-declaration': () => `Declaration cannot be empty`, 'invalid-css-empty-declaration': () => `Declaration cannot be empty`,
'invalid-css-global-block-list': () =>
`A :global {...} block cannot be part of a selector list with more than one item`,
'invalid-css-global-block-modifier': () =>
`A :global {...} block cannot modify an existing selector`,
/** @param {string} name */
'invalid-css-global-block-combinator': (name) =>
`A :global {...} block cannot follow a ${name} combinator`,
'invalid-css-global-block-declaration': () =>
`A :global {...} block can only contain rules, not declarations`,
'invalid-css-global-placement': () => 'invalid-css-global-placement': () =>
`:global(...) can be at the start or end of a selector sequence, but not in the middle`, `:global(...) can be at the start or end of a selector sequence, but not in the middle`,
'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`, 'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`,

@ -3,7 +3,6 @@ import { error } from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/; const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_CLOSING_BRACKET = /[\s\]]/; const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/; const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF = const REGEX_NTH_OF =
@ -116,7 +115,8 @@ function read_rule(parser) {
end: parser.index, end: parser.index,
metadata: { metadata: {
parent_rule: null, parent_rule: null,
has_local_selectors: false has_local_selectors: false,
is_global_block: false
} }
}; };
} }
@ -252,8 +252,6 @@ function read_selector(parser, inside_pseudo_class = false) {
if (parser.eat('(')) { if (parser.eat('(')) {
args = read_selector_list(parser, true); args = read_selector_list(parser, true);
parser.eat(')', true); parser.eat(')', true);
} else if (name === 'global') {
error(parser.index, 'invalid-css-global-selector');
} }
relative_selector.selectors.push({ relative_selector.selectors.push({

@ -69,6 +69,17 @@ const analysis_visitors = {
Rule(node, context) { Rule(node, context) {
node.metadata.parent_rule = context.state.rule; node.metadata.parent_rule = context.state.rule;
// `:global {...}` or `div :global {...}`
node.metadata.is_global_block = node.prelude.children.some((selector) => {
const last = selector.children[selector.children.length - 1];
const s = last.selectors[last.selectors.length - 1];
if (s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null) {
return true;
}
});
context.next({ context.next({
...context.state, ...context.state,
rule: node rule: node
@ -84,6 +95,39 @@ const analysis_visitors = {
/** @type {Visitors} */ /** @type {Visitors} */
const validation_visitors = { const validation_visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
if (node.prelude.children.length > 1) {
error(node.prelude, 'invalid-css-global-block-list');
}
const complex_selector = node.prelude.children[0];
const relative_selector = complex_selector.children[complex_selector.children.length - 1];
if (relative_selector.selectors.length > 1) {
error(
relative_selector.selectors[relative_selector.selectors.length - 1],
'invalid-css-global-block-modifier'
);
}
if (relative_selector.combinator && relative_selector.combinator.name !== ' ') {
error(
relative_selector,
'invalid-css-global-block-combinator',
relative_selector.combinator.name
);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (declaration) {
error(declaration, 'invalid-css-global-block-declaration');
}
}
context.next();
},
ComplexSelector(node, context) { ComplexSelector(node, context) {
// ensure `:global(...)` is not used in the middle of a selector // ensure `:global(...)` is not used in the middle of a selector
{ {

@ -1,7 +1,6 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js'; import { get_possible_values } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';
/** /**
* @typedef {{ * @typedef {{
@ -60,6 +59,13 @@ export function prune(stylesheet, element) {
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */ /** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = { const visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node, context) { ComplexSelector(node, context) {
const selectors = truncate(node); const selectors = truncate(node);
const inner = selectors[selectors.length - 1]; const inner = selectors[selectors.length - 1];

@ -30,5 +30,12 @@ const visitors = {
} }
context.next(); context.next();
},
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
} }
}; };

@ -116,7 +116,7 @@ const visitors = {
} }
} }
}, },
Rule(node, { state, next }) { Rule(node, { state, next, visit }) {
// keep empty rules in dev, because it's convenient to // keep empty rules in dev, because it's convenient to
// see them in devtools // see them in devtools
if (!state.dev && is_empty(node)) { if (!state.dev && is_empty(node)) {
@ -134,6 +134,26 @@ const visitors = {
return; return;
} }
if (node.metadata.is_global_block) {
const selector = node.prelude.children[0];
if (selector.children.length === 1) {
// `:global {...}`
state.code.prependRight(node.start, '/* ');
state.code.appendLeft(node.block.start + 1, '*/');
state.code.prependRight(node.block.end - 1, '/*');
state.code.appendLeft(node.block.end, '*/');
// don't recurse into selector or body
return;
}
// don't recurse into body
visit(node.prelude);
return;
}
next(); next();
}, },
SelectorList(node, { state, next, path }) { SelectorList(node, { state, next, path }) {
@ -275,6 +295,10 @@ const visitors = {
/** @param {import('#compiler').Css.Rule} rule */ /** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) { function is_empty(rule) {
if (rule.metadata.is_global_block) {
return rule.block.children.length === 0;
}
for (const child of rule.block.children) { for (const child of rule.block.children) {
if (child.type === 'Declaration') { if (child.type === 'Declaration') {
return false; return false;

@ -33,6 +33,7 @@ export namespace Css {
metadata: { metadata: {
parent_rule: null | Rule; parent_rule: null | Rule;
has_local_selectors: boolean; has_local_selectors: boolean;
is_global_block: boolean;
}; };
} }

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-css-global-block-combinator',
message: 'A :global {...} block cannot follow a > combinator',
position: [12, 21]
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-css-global-block-declaration',
message: 'A :global {...} block can only contain rules, not declarations',
position: [24, 34]
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-css-global-block-modifier',
message: 'A :global {...} block cannot modify an existing selector',
position: [14, 21]
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-css-global-block-list',
message: 'A :global {...} block cannot be part of a selector list with more than one item',
position: [9, 31]
}
});

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-css-global-selector',
message: ':global(...) must contain exactly one selector',
position: [16, 16]
}
});

@ -0,0 +1,21 @@
import { test } from '../../test';
export default test({
warnings: [
{
filename: 'SvelteComponent.svelte',
code: 'css-unused-selector',
message: 'Unused CSS selector ".unused :global"',
start: {
line: 16,
column: 1,
character: 128
},
end: {
line: 16,
column: 16,
character: 143
}
}
]
});

@ -0,0 +1,17 @@
/* :global {*/
.x {
color: green;
}
/*}*/
div.svelte-xyz {
.y {
color: green;
}
}
/* (unused) .unused :global {
.z {
color: red;
}
}*/

@ -0,0 +1,21 @@
<div>{@html whatever}</div>
<style>
:global {
.x {
color: green;
}
}
div :global {
.y {
color: green;
}
}
.unused :global {
.z {
color: red;
}
}
</style>

@ -1122,6 +1122,7 @@ declare module 'svelte/compiler' {
metadata: { metadata: {
parent_rule: null | Rule; parent_rule: null | Rule;
has_local_selectors: boolean; has_local_selectors: boolean;
is_global_block: boolean;
}; };
} }

Loading…
Cancel
Save