Skip to content

Commit

Permalink
feat: implement :global {...} CSS blocks (#11276)
Browse files Browse the repository at this point in the history
* feat: implement `:global {...}` CSS blocks

* tests for compiler errors

* regenerate types

* lint
  • Loading branch information
Rich-Harris committed Apr 22, 2024
1 parent 11c7cd5 commit 9721d56
Show file tree
Hide file tree
Showing 21 changed files with 208 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-apples-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: implement `:global {...}` CSS blocks
9 changes: 9 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ const css = {
/** @param {string} message */
'css-parse-error': (message) => message,
'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': () =>
`: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`,
Expand Down
6 changes: 2 additions & 4 deletions packages/svelte/src/compiler/phases/1-parse/read/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { error } from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
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_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF =
Expand Down Expand Up @@ -116,7 +115,8 @@ function read_rule(parser) {
end: parser.index,
metadata: {
parent_rule: null,
has_local_selectors: false
has_local_selectors: false,
is_global_block: false
}
};
}
Expand Down Expand Up @@ -252,8 +252,6 @@ function read_selector(parser, inside_pseudo_class = false) {
if (parser.eat('(')) {
args = read_selector_list(parser, true);
parser.eat(')', true);
} else if (name === 'global') {
error(parser.index, 'invalid-css-global-selector');
}

relative_selector.selectors.push({
Expand Down
44 changes: 44 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ const analysis_visitors = {
Rule(node, context) {
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.state,
rule: node
Expand All @@ -84,6 +95,39 @@ const analysis_visitors = {

/** @type {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) {
// ensure `:global(...)` is not used in the middle of a selector
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';

/**
* @typedef {{
Expand Down Expand Up @@ -60,6 +59,13 @@ export function prune(stylesheet, element) {

/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node, context) {
const selectors = truncate(node);
const inner = selectors[selectors.length - 1];
Expand Down
7 changes: 7 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,12 @@ const visitors = {
}

context.next();
},
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
}
};
26 changes: 25 additions & 1 deletion packages/svelte/src/compiler/phases/3-transform/css/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// see them in devtools
if (!state.dev && is_empty(node)) {
Expand All @@ -134,6 +134,26 @@ const visitors = {
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();
},
SelectorList(node, { state, next, path }) {
Expand Down Expand Up @@ -275,6 +295,10 @@ const visitors = {

/** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) {
if (rule.metadata.is_global_block) {
return rule.block.children.length === 0;
}

for (const child of rule.block.children) {
if (child.type === 'Declaration') {
return false;
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/types/css.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export namespace Css {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
is_global_block: boolean;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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]
}
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<style>
:global {}
.x > :global {}
</style>
Original file line number Diff line number Diff line change
@@ -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]
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<style>
.x :global {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -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]
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<style>
.x .y:global {}
</style>
Original file line number Diff line number Diff line change
@@ -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]
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<style>
.x :global, .y :global {}
</style>

This file was deleted.

21 changes: 21 additions & 0 deletions packages/svelte/tests/css/samples/global-block/_config.js
Original file line number Diff line number Diff line change
@@ -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
}
}
]
});
17 changes: 17 additions & 0 deletions packages/svelte/tests/css/samples/global-block/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* :global {*/
.x {
color: green;
}
/*}*/

div.svelte-xyz {
.y {
color: green;
}
}

/* (unused) .unused :global {
.z {
color: red;
}
}*/
21 changes: 21 additions & 0 deletions packages/svelte/tests/css/samples/global-block/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div>{@html whatever}</div>

<style>
:global {
.x {
color: green;
}
}
div :global {
.y {
color: green;
}
}
.unused :global {
.z {
color: red;
}
}
</style>
1 change: 1 addition & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,7 @@ declare module 'svelte/compiler' {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
is_global_block: boolean;
};
}

Expand Down

0 comments on commit 9721d56

Please sign in to comment.