chore: tidy up CSS stuff (#10478)

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10487/head
Rich Harris 2 years ago committed by GitHub
parent 643edc34db
commit 87d4b12620
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -47,7 +47,7 @@ export class Parser {
throw new TypeError('Template must be a string'); throw new TypeError('Template must be a string');
} }
this.template = template.trimRight(); this.template = template.trimEnd();
let match_lang; let match_lang;
@ -147,9 +147,9 @@ export class Parser {
/** /**
* @param {string} str * @param {string} str
* @param {boolean} [required] * @param {boolean} required
*/ */
eat(str, required) { eat(str, required = false) {
if (this.match(str)) { if (this.match(str)) {
this.index += str.length; this.index += str.length;
return true; return true;

@ -1,6 +1,7 @@
import { get_possible_values } from './gather_possible_values.js'; import { get_possible_values } from './gather_possible_values.js';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js'; import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
import { Stylesheet } from './Stylesheet.js';
const NO_MATCH = 'NO_MATCH'; const NO_MATCH = 'NO_MATCH';
const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; const POSSIBLE_MATCH = 'POSSIBLE_MATCH';
@ -22,7 +23,7 @@ export default class Selector {
/** @type {import('#compiler').Css.Selector} */ /** @type {import('#compiler').Css.Selector} */
node; node;
/** @type {import('./Stylesheet.js').default} */ /** @type {import('./Stylesheet.js').Stylesheet} */
stylesheet; stylesheet;
/** @type {Block[]} */ /** @type {Block[]} */
@ -31,39 +32,28 @@ export default class Selector {
/** @type {Block[]} */ /** @type {Block[]} */
local_blocks; local_blocks;
/** @type {boolean} */ used = false;
used;
/** /**
* @param {import('#compiler').Css.Selector} node * @param {import('#compiler').Css.Selector} node
* @param {import('./Stylesheet.js').default} stylesheet * @param {import('./Stylesheet.js').Stylesheet} stylesheet
*/ */
constructor(node, stylesheet) { constructor(node, stylesheet) {
this.node = node; this.node = node;
this.stylesheet = stylesheet; this.stylesheet = stylesheet;
this.blocks = group_selectors(node); this.blocks = group_selectors(node);
// take trailing :global(...) selectors out of consideration // take trailing :global(...) selectors out of consideration
let i = this.blocks.length; const i = this.blocks.findLastIndex((block) => !block.can_ignore());
while (i > 0) { this.local_blocks = this.blocks.slice(0, i + 1);
if (!this.blocks[i - 1].global) break;
i -= 1; // if we have a `:root {...}` or `:global(...) {...}` selector, we need to mark
} // this selector as `used` even if the component doesn't contain any nodes
this.local_blocks = this.blocks.slice(0, i); this.used = this.local_blocks.length === 0;
const host_only = this.blocks.length === 1 && this.blocks[0].host;
const root_only = this.blocks.length === 1 && this.blocks[0].root;
this.used = this.local_blocks.length === 0 || host_only || root_only;
} }
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) { apply(node) {
/** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */ if (apply_selector(this.local_blocks.slice(), node, this.stylesheet)) {
const to_encapsulate = [];
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
if (to_encapsulate.length > 0) {
to_encapsulate.forEach(({ node, block }) => {
this.stylesheet.nodes_with_css_class.add(node);
block.should_encapsulate = true;
});
this.used = true; this.used = true;
} }
} }
@ -130,6 +120,13 @@ export default class Selector {
/** @param {import('../../types.js').ComponentAnalysis} analysis */ /** @param {import('../../types.js').ComponentAnalysis} analysis */
validate(analysis) { validate(analysis) {
this.validate_global_placement();
this.validate_global_with_multiple_selectors();
this.validate_global_compound_selector();
this.validate_invalid_combinator_without_selector(analysis);
}
validate_global_placement() {
let start = 0; let start = 0;
let end = this.blocks.length; let end = this.blocks.length;
for (; start < end; start += 1) { for (; start < end; start += 1) {
@ -143,9 +140,6 @@ export default class Selector {
error(this.blocks[i].selectors[0], 'invalid-css-global-placement'); error(this.blocks[i].selectors[0], 'invalid-css-global-placement');
} }
} }
this.validate_global_with_multiple_selectors();
this.validate_global_compound_selector();
this.validate_invalid_combinator_without_selector(analysis);
} }
validate_global_with_multiple_selectors() { validate_global_with_multiple_selectors() {
@ -207,10 +201,10 @@ export default class Selector {
/** /**
* @param {Block[]} blocks * @param {Block[]} blocks
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node
* @param {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} to_encapsulate * @param {Stylesheet} stylesheet
* @returns {boolean} * @returns {boolean}
*/ */
function apply_selector(blocks, node, to_encapsulate) { function apply_selector(blocks, node, stylesheet) {
const block = blocks.pop(); const block = blocks.pop();
if (!block) return false; if (!block) return false;
if (!node) { if (!node) {
@ -224,11 +218,22 @@ function apply_selector(blocks, node, to_encapsulate) {
return false; return false;
} }
if (applies === UNKNOWN_SELECTOR) { /**
to_encapsulate.push({ node, block }); * Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {Block} block
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
*/
function mark(block, node) {
block.should_encapsulate = true;
stylesheet.nodes_with_css_class.add(node);
return true; return true;
} }
if (applies === UNKNOWN_SELECTOR) {
return mark(block, node);
}
if (block.combinator) { if (block.combinator) {
if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') { if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') {
for (const ancestor_block of blocks) { for (const ancestor_block of blocks) {
@ -236,31 +241,29 @@ function apply_selector(blocks, node, to_encapsulate) {
continue; continue;
} }
if (ancestor_block.host) { if (ancestor_block.host) {
to_encapsulate.push({ node, block }); return mark(block, node);
return true;
} }
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */ /** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
let parent = node; let parent = node;
let matched = false;
while ((parent = get_element_parent(parent))) { while ((parent = get_element_parent(parent))) {
if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) { if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) {
to_encapsulate.push({ node: parent, block: ancestor_block }); mark(ancestor_block, parent);
matched = true;
} }
} }
if (to_encapsulate.length) { if (matched) {
to_encapsulate.push({ node, block }); return mark(block, node);
return true;
} }
} }
if (blocks.every((block) => block.global)) { if (blocks.every((block) => block.global)) {
to_encapsulate.push({ node, block }); return mark(block, node);
return true;
} }
return false; return false;
} else if (block.combinator.name === '>') { } else if (block.combinator.name === '>') {
const has_global_parent = blocks.every((block) => block.global); const has_global_parent = blocks.every((block) => block.global);
if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) { if (has_global_parent || apply_selector(blocks, get_element_parent(node), stylesheet)) {
to_encapsulate.push({ node, block }); return mark(block, node);
return true;
} }
return false; return false;
} else if (block.combinator.name === '+' || block.combinator.name === '~') { } else if (block.combinator.name === '+' || block.combinator.name === '~') {
@ -274,23 +277,22 @@ function apply_selector(blocks, node, to_encapsulate) {
if (siblings.size === 0 && get_element_parent(node) !== null) { if (siblings.size === 0 && get_element_parent(node) !== null) {
return false; return false;
} }
to_encapsulate.push({ node, block }); return mark(block, node);
return true;
} }
for (const possible_sibling of siblings.keys()) { for (const possible_sibling of siblings.keys()) {
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) { if (apply_selector(blocks.slice(), possible_sibling, stylesheet)) {
to_encapsulate.push({ node, block }); mark(block, node);
has_match = true; has_match = true;
} }
} }
return has_match; return has_match;
} }
// TODO other combinators // TODO other combinators
to_encapsulate.push({ node, block }); return mark(block, node);
return true;
} }
to_encapsulate.push({ node, block });
return true; return mark(block, node);
} }
const regex_backslash_and_following_character = /\\(.)/g; const regex_backslash_and_following_character = /\\(.)/g;
@ -815,6 +817,11 @@ class Block {
this.selectors.push(selector); this.selectors.push(selector);
this.end = selector.end; this.end = selector.end;
} }
can_ignore() {
return this.global || this.host || this.root;
}
get global() { get global() {
return ( return (
this.selectors.length >= 1 && this.selectors.length >= 1 &&

@ -52,19 +52,19 @@ class Rule {
/** @type {import('./Selector.js').default[]} */ /** @type {import('./Selector.js').default[]} */
selectors; selectors;
/** @type {Declaration[]} */
declarations;
/** @type {import('#compiler').Css.Rule} */ /** @type {import('#compiler').Css.Rule} */
node; node;
/** @type {Atrule | undefined} */ /** @type {Stylesheet | Atrule} */
parent; parent;
/** @type {Declaration[]} */
declarations;
/** /**
* @param {import('#compiler').Css.Rule} node * @param {import('#compiler').Css.Rule} node
* @param {any} stylesheet * @param {any} stylesheet
* @param {Atrule | undefined} parent * @param {Stylesheet | Atrule} parent
*/ */
constructor(node, stylesheet, parent) { constructor(node, stylesheet, parent) {
this.node = node; this.node = node;
@ -81,16 +81,24 @@ class Rule {
this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here? this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here?
} }
/** @param {boolean} dev */ /** @returns {boolean} */
is_used(dev) { is_empty() {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) if (this.declarations.length > 0) return false;
return true;
}
/** @returns {boolean} */
is_used() {
if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) {
return true; return true;
}
// keep empty rules in dev, because it's convenient to for (const selector of this.selectors) {
// see them in devtools if (selector.used) return true;
if (this.declarations.length === 0) return dev; }
return this.selectors.some((s) => s.used); return false;
} }
/** /**
@ -99,7 +107,7 @@ class Rule {
* @param {Map<string, string>} keyframes * @param {Map<string, string>} keyframes
*/ */
transform(code, id, keyframes) { transform(code, id, keyframes) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) {
return; return;
} }
@ -127,19 +135,16 @@ class Rule {
* @param {boolean} dev * @param {boolean} dev
*/ */
prune(code, dev) { prune(code, dev) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) {
return; return;
} }
// 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 (this.declarations.length === 0) { if (!dev && this.is_empty()) {
if (!dev) { code.prependRight(this.node.start, '/* (empty) ');
code.prependRight(this.node.start, '/* (empty) '); code.appendLeft(this.node.end, '*/');
code.appendLeft(this.node.end, '*/'); escape_comment_close(this.node, code);
escape_comment_close(this.node, code);
}
return; return;
} }
@ -268,8 +273,11 @@ class Atrule {
} }
} }
/** @param {boolean} _dev */ is_empty() {
is_used(_dev) { return false; // TODO
}
is_used() {
return true; // TODO return true; // TODO
} }
@ -326,7 +334,7 @@ class Atrule {
} }
} }
export default class Stylesheet { export class Stylesheet {
/** @type {import('#compiler').Style | null} */ /** @type {import('#compiler').Style | null} */
ast; ast;
@ -375,53 +383,35 @@ export default class Stylesheet {
this.has_styles = true; this.has_styles = true;
const state = { const state = {
/** @type {Atrule | undefined} */ /** @type {Stylesheet | Atrule | Rule} */
atrule: undefined current: this
}; };
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, { walk(/** @type {import('#compiler').Css.Node} */ (ast), state, {
Atrule: (node, context) => { Atrule: (node, context) => {
const atrule = new Atrule(node); const atrule = new Atrule(node);
if (context.state.atrule) {
context.state.atrule.children.push(atrule);
} else {
this.children.push(atrule);
}
if (is_keyframes_node(node)) { if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) { if (!node.prelude.startsWith('-global-')) {
this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`); this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`);
} }
} else if (node.block) {
/** @type {Declaration[]} */
const declarations = [];
for (const child of node.block.children) {
if (child.type === 'Declaration') {
declarations.push(new Declaration(child));
}
}
if (declarations.length > 0) {
push_array(atrule.declarations, declarations);
}
} }
context.next({ // @ts-expect-error temporary, until nesting is implemented
...context.state, context.state.current.children.push(atrule);
atrule context.next({ current: atrule });
}); },
Declaration: (node, context) => {
const declaration = new Declaration(node);
/** @type {Atrule | Rule} */ (context.state.current).declarations.push(declaration);
}, },
Rule: (node, context) => { Rule: (node, context) => {
const rule = new Rule(node, this, context.state.atrule); // @ts-expect-error temporary, until nesting is implemented
if (context.state.atrule) { const rule = new Rule(node, this, context.state.current);
context.state.atrule.children.push(rule);
} else {
this.children.push(rule);
}
context.next(); // @ts-expect-error temporary, until nesting is implemented
context.state.current.children.push(rule);
context.next({ current: rule });
} }
}); });
} }

@ -13,7 +13,7 @@ import * as b from '../../utils/builders.js';
import { ReservedKeywords, Runes, SVGElements } from '../constants.js'; import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js'; import { merge } from '../visitors.js';
import Stylesheet from './css/Stylesheet.js'; import { Stylesheet } from './css/Stylesheet.js';
import { validation_legacy, validation_runes, validation_runes_js } from './validation.js'; import { validation_legacy, validation_runes, validation_runes_js } from './validation.js';
import { warn } from '../../warnings.js'; import { warn } from '../../warnings.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js';

@ -7,7 +7,7 @@ import type {
SvelteOptions SvelteOptions
} from '#compiler'; } from '#compiler';
import type { Identifier, LabeledStatement, Program } from 'estree'; import type { Identifier, LabeledStatement, Program } from 'estree';
import type Stylesheet from './2-analyze/css/Stylesheet.js'; import { Stylesheet } from './2-analyze/css/Stylesheet.js';
import type { Scope, ScopeRoot } from './scope.js'; import type { Scope, ScopeRoot } from './scope.js';
export interface Js { export interface Js {

@ -99,4 +99,4 @@ export interface Declaration extends BaseNode {
} }
// for zimmerframe // for zimmerframe
export type Node = Style | Rule | Atrule; export type Node = Style | Rule | Atrule | Declaration;

@ -1,5 +1,6 @@
div.svelte-xyz { div.svelte-xyz {
@apply --funky-div; @apply --funky-div;
color: red;
} }
/* (empty) div.svelte-xyz { /* (empty) div.svelte-xyz {

@ -3,6 +3,7 @@
<style> <style>
div { div {
@apply --funky-div; @apply --funky-div;
color: red;
} }
div { div {

@ -1,3 +1,4 @@
div.svelte-xyz { div.svelte-xyz {
@apply --funky-div; @apply --funky-div;
color: red;
} }

@ -3,5 +3,6 @@
<style> <style>
div { div {
@apply --funky-div; @apply --funky-div;
color: red;
} }
</style> </style>

Loading…
Cancel
Save