mirror of https://github.com/sveltejs/svelte
chore: use zimmerframe for CSS analysis/transformation (#10482)
* fix type * parse selectors properly the first time * partial fix * fix * start moving CSS validation into analysis phase * finish moving validation * fix tests * regenerate types * start porting scoping logic etc * move Style to Css.StyleSheet * some encouraging progress * more progress * more progress * fix a bunch of cases * more fixes * tweak * almost there * keyframes * fix * all CSS tests passing * legacy stuff * all tests passing * delete old code * regenerate types * defer analysis, address TODO * unused * tidy up * rename stuff * read selectors before combinators --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/10479/head
parent
f8ff2b6ea3
commit
cec3540ac2
@ -1,549 +0,0 @@
|
||||
import MagicString from 'magic-string';
|
||||
import { walk } from 'zimmerframe';
|
||||
import { ComplexSelector } from './Selector.js';
|
||||
import { hash } from './utils.js';
|
||||
import { create_attribute } from '../phases/nodes.js'; // TODO move this
|
||||
import { merge_with_preprocessor_map } from '../utils/mapped_code.js';
|
||||
|
||||
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
|
||||
const regex_name_boundary = /^[\s,;}]$/;
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
function remove_css_prefix(name) {
|
||||
return name.replace(regex_css_browser_prefix, '');
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.Atrule} node */
|
||||
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('#compiler').Css.Rule} node
|
||||
* @param {MagicString} code
|
||||
*/
|
||||
function escape_comment_close(node, code) {
|
||||
let escaped = false;
|
||||
let in_comment = false;
|
||||
|
||||
for (let i = node.start; i < node.end; i++) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else {
|
||||
const char = code.original[i];
|
||||
if (in_comment) {
|
||||
if (char === '*' && code.original[i + 1] === '/') {
|
||||
code.prependRight(++i, '\\');
|
||||
in_comment = false;
|
||||
}
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === '/' && code.original[++i] === '*') {
|
||||
in_comment = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Rule {
|
||||
/** @type {ComplexSelector[]} */
|
||||
selectors;
|
||||
|
||||
/** @type {import('#compiler').Css.Rule} */
|
||||
node;
|
||||
|
||||
/** @type {Stylesheet | Atrule} */
|
||||
parent;
|
||||
|
||||
/** @type {Declaration[]} */
|
||||
declarations;
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').Css.Rule} node
|
||||
* @param {any} stylesheet
|
||||
* @param {Stylesheet | Atrule} parent
|
||||
*/
|
||||
constructor(node, stylesheet, parent) {
|
||||
this.node = node;
|
||||
this.parent = parent;
|
||||
this.selectors = node.prelude.children.map((node) => new ComplexSelector(node, stylesheet));
|
||||
|
||||
this.declarations = /** @type {import('#compiler').Css.Declaration[]} */ (
|
||||
node.block.children
|
||||
).map((node) => new Declaration(node));
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here?
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
is_empty() {
|
||||
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;
|
||||
}
|
||||
|
||||
for (const selector of this.selectors) {
|
||||
if (selector.used) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {string} id
|
||||
* @param {Map<string, string>} keyframes
|
||||
*/
|
||||
transform(code, id, keyframes) {
|
||||
if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modifier = `.${id}`;
|
||||
this.selectors.forEach((selector) => selector.transform(code, modifier));
|
||||
this.declarations.forEach((declaration) => declaration.transform(code, keyframes));
|
||||
}
|
||||
|
||||
/** @param {import('../phases/types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
this.selectors.forEach((selector) => {
|
||||
selector.validate(analysis);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {(selector: ComplexSelector) => void} handler */
|
||||
warn_on_unused_selector(handler) {
|
||||
this.selectors.forEach((selector) => {
|
||||
if (!selector.used) handler(selector);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MagicString} code
|
||||
* @param {boolean} dev
|
||||
*/
|
||||
prune(code, dev) {
|
||||
if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keep empty rules in dev, because it's convenient to
|
||||
// see them in devtools
|
||||
if (!dev && this.is_empty()) {
|
||||
code.prependRight(this.node.start, '/* (empty) ');
|
||||
code.appendLeft(this.node.end, '*/');
|
||||
escape_comment_close(this.node, code);
|
||||
return;
|
||||
}
|
||||
|
||||
const used = this.selectors.filter((s) => s.used);
|
||||
|
||||
if (used.length === 0) {
|
||||
code.prependRight(this.node.start, '/* (unused) ');
|
||||
code.appendLeft(this.node.end, '*/');
|
||||
escape_comment_close(this.node, code);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (used.length < this.selectors.length) {
|
||||
let pruning = false;
|
||||
let last = this.selectors[0].node.start;
|
||||
|
||||
for (let i = 0; i < this.selectors.length; i += 1) {
|
||||
const selector = this.selectors[i];
|
||||
|
||||
if (selector.used === pruning) {
|
||||
if (pruning) {
|
||||
let i = selector.node.start;
|
||||
while (code.original[i] !== ',') i--;
|
||||
|
||||
code.overwrite(i, i + 1, '*/');
|
||||
} else {
|
||||
if (i === 0) {
|
||||
code.prependRight(selector.node.start, '/* (unused) ');
|
||||
} else {
|
||||
code.overwrite(last, selector.node.start, ' /* (unused) ');
|
||||
}
|
||||
}
|
||||
|
||||
pruning = !pruning;
|
||||
}
|
||||
|
||||
last = selector.node.end;
|
||||
}
|
||||
|
||||
if (pruning) {
|
||||
code.appendLeft(last, '*/');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Declaration {
|
||||
/** @type {import('#compiler').Css.Declaration} */
|
||||
node;
|
||||
|
||||
/** @param {import('#compiler').Css.Declaration} node */
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {Map<string, string>} keyframes
|
||||
*/
|
||||
transform(code, keyframes) {
|
||||
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
|
||||
if (property === 'animation' || property === 'animation-name') {
|
||||
let index = this.node.start + this.node.property.length + 1;
|
||||
let name = '';
|
||||
|
||||
while (index < code.original.length) {
|
||||
const character = code.original[index];
|
||||
|
||||
if (regex_name_boundary.test(character)) {
|
||||
const keyframe = keyframes.get(name);
|
||||
|
||||
if (keyframe) {
|
||||
code.update(index - name.length, index, keyframe);
|
||||
}
|
||||
|
||||
if (character === ';' || character === '}') {
|
||||
break;
|
||||
}
|
||||
|
||||
name = '';
|
||||
} else {
|
||||
name += character;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Atrule {
|
||||
/** @type {import('#compiler').Css.Atrule} */
|
||||
node;
|
||||
|
||||
/** @type {Array<Atrule | Rule>} */
|
||||
children;
|
||||
|
||||
/** @type {Declaration[]} */
|
||||
declarations;
|
||||
|
||||
/** @param {import('#compiler').Css.Atrule} node */
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
this.children = [];
|
||||
this.declarations = [];
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
if (
|
||||
this.node.name === 'container' ||
|
||||
this.node.name === 'media' ||
|
||||
this.node.name === 'supports' ||
|
||||
this.node.name === 'layer'
|
||||
) {
|
||||
this.children.forEach((child) => {
|
||||
child.apply(node);
|
||||
});
|
||||
} else if (is_keyframes_node(this.node)) {
|
||||
/** @type {Rule[]} */ (this.children).forEach((rule) => {
|
||||
rule.selectors.forEach((selector) => {
|
||||
selector.used = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
is_empty() {
|
||||
return false; // TODO
|
||||
}
|
||||
|
||||
is_used() {
|
||||
return true; // TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {string} id
|
||||
* @param {Map<string, string>} keyframes
|
||||
*/
|
||||
transform(code, id, keyframes) {
|
||||
if (is_keyframes_node(this.node)) {
|
||||
let start = this.node.start + this.node.name.length + 1;
|
||||
while (code.original[start] === ' ') start += 1;
|
||||
let end = start;
|
||||
while (code.original[end] !== '{' && code.original[end] !== ' ') end += 1;
|
||||
|
||||
if (this.node.prelude.startsWith('-global-')) {
|
||||
code.remove(start, start + 8);
|
||||
/** @type {Rule[]} */ (this.children).forEach((rule) => {
|
||||
rule.selectors.forEach((selector) => {
|
||||
selector.used = true;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const keyframe = /** @type {string} */ (keyframes.get(this.node.prelude));
|
||||
code.update(start, end, keyframe);
|
||||
}
|
||||
}
|
||||
this.children.forEach((child) => {
|
||||
child.transform(code, id, keyframes);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('../phases/types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
this.children.forEach((child) => {
|
||||
child.validate(analysis);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {(selector: ComplexSelector) => void} handler */
|
||||
warn_on_unused_selector(handler) {
|
||||
if (this.node.name !== 'media') return;
|
||||
this.children.forEach((child) => {
|
||||
child.warn_on_unused_selector(handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MagicString} code
|
||||
* @param {boolean} dev
|
||||
*/
|
||||
prune(code, dev) {
|
||||
// TODO prune children
|
||||
}
|
||||
}
|
||||
|
||||
export class Stylesheet {
|
||||
/** @type {import('#compiler').Style | null} */
|
||||
ast;
|
||||
|
||||
/** @type {string} Path of Svelte file the CSS is in */
|
||||
filename;
|
||||
|
||||
/** @type {boolean} */
|
||||
has_styles;
|
||||
|
||||
/** @type {string} */
|
||||
id;
|
||||
|
||||
/** @type {Array<Rule | Atrule>} */
|
||||
children = [];
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
keyframes = new Map();
|
||||
|
||||
/** @type {Set<import('#compiler').RegularElement | import('#compiler').SvelteElement>} */
|
||||
nodes_with_css_class = new Set();
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* ast: import('#compiler').Style | null;
|
||||
* filename: string;
|
||||
* component_name: string;
|
||||
* get_css_hash: import('#compiler').CssHashGetter;
|
||||
* }} params
|
||||
*/
|
||||
constructor({ ast, component_name, filename, get_css_hash }) {
|
||||
this.ast = ast;
|
||||
this.filename = filename;
|
||||
|
||||
if (!ast || ast.children.length === 0) {
|
||||
this.has_styles = false;
|
||||
this.id = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = get_css_hash({
|
||||
filename,
|
||||
name: component_name,
|
||||
css: ast.content.styles,
|
||||
hash
|
||||
});
|
||||
this.has_styles = true;
|
||||
|
||||
const state = {
|
||||
/** @type {Stylesheet | Atrule | Rule} */
|
||||
current: this
|
||||
};
|
||||
|
||||
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, {
|
||||
Atrule: (node, context) => {
|
||||
const atrule = new Atrule(node);
|
||||
|
||||
if (is_keyframes_node(node)) {
|
||||
if (!node.prelude.startsWith('-global-')) {
|
||||
this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error temporary, until nesting is implemented
|
||||
context.state.current.children.push(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) => {
|
||||
// @ts-expect-error temporary, until nesting is implemented
|
||||
const rule = new Rule(node, this, context.state.current);
|
||||
|
||||
// @ts-expect-error temporary, until nesting is implemented
|
||||
context.state.current.children.push(rule);
|
||||
context.next({ current: rule });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
if (!this.has_styles) return;
|
||||
for (let i = 0; i < this.children.length; i += 1) {
|
||||
const child = this.children[i];
|
||||
child.apply(node);
|
||||
}
|
||||
}
|
||||
/** @param {boolean} is_dom_mode */
|
||||
reify(is_dom_mode) {
|
||||
nodes: for (const node of this.nodes_with_css_class) {
|
||||
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
|
||||
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
|
||||
if (node.type === 'SvelteElement' && is_dom_mode) continue;
|
||||
|
||||
/** @type {import('#compiler').Attribute | undefined} */
|
||||
let class_attribute = undefined;
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'SpreadAttribute') {
|
||||
// The spread method appends the hash to the end of the class attribute on its own
|
||||
continue nodes;
|
||||
}
|
||||
|
||||
if (attribute.type !== 'Attribute') continue;
|
||||
if (attribute.name.toLowerCase() !== 'class') continue;
|
||||
|
||||
class_attribute = attribute;
|
||||
}
|
||||
|
||||
if (class_attribute && class_attribute.value !== true) {
|
||||
const chunks = class_attribute.value;
|
||||
|
||||
if (chunks.length === 1 && chunks[0].type === 'Text') {
|
||||
chunks[0].data += ` ${this.id}`;
|
||||
} else {
|
||||
chunks.push({
|
||||
type: 'Text',
|
||||
data: ` ${this.id}`,
|
||||
raw: ` ${this.id}`,
|
||||
start: -1,
|
||||
end: -1,
|
||||
parent: null
|
||||
});
|
||||
}
|
||||
} else {
|
||||
node.attributes.push(
|
||||
create_attribute('class', -1, -1, [
|
||||
{ type: 'Text', data: this.id, raw: this.id, parent: null, start: -1, end: -1 }
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {import('#compiler').ValidatedCompileOptions} options
|
||||
*/
|
||||
render(source, options) {
|
||||
// TODO neaten this up
|
||||
if (!this.ast) throw new Error('Unexpected error');
|
||||
|
||||
const code = new MagicString(source);
|
||||
|
||||
// Generate source mappings for the style sheet nodes we have.
|
||||
// Note that resolution is a bit more coarse than in Svelte 4 because
|
||||
// our own CSS AST is not as detailed with regards to the node values.
|
||||
walk(/** @type {import('#compiler').Css.Node} */ (this.ast), null, {
|
||||
_: (node, { next }) => {
|
||||
code.addSourcemapLocation(node.start);
|
||||
code.addSourcemapLocation(node.end);
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
for (const child of this.children) {
|
||||
child.transform(code, this.id, this.keyframes);
|
||||
}
|
||||
|
||||
code.remove(0, this.ast.content.start);
|
||||
|
||||
for (const child of this.children) {
|
||||
child.prune(code, options.dev);
|
||||
}
|
||||
|
||||
code.remove(/** @type {number} */ (this.ast.content.end), source.length);
|
||||
|
||||
const css = {
|
||||
code: code.toString(),
|
||||
map: code.generateMap({
|
||||
// include source content; makes it easier/more robust looking up the source map code
|
||||
includeContent: true,
|
||||
// generateMap takes care of calculating source relative to file
|
||||
source: this.filename,
|
||||
file: options.cssOutputFilename || this.filename
|
||||
})
|
||||
};
|
||||
merge_with_preprocessor_map(css, options, css.map.sources[0]);
|
||||
if (options.dev && options.css === 'injected' && css.code) {
|
||||
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
/** @param {import('../phases/types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
this.children.forEach((child) => {
|
||||
child.validate(analysis);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('../phases/types.js').ComponentAnalysis} analysis */
|
||||
warn_on_unused_selectors(analysis) {
|
||||
// const ignores = !this.ast
|
||||
// ? []
|
||||
// : extract_ignores_above_position(this.ast.css.start, this.ast.html.children);
|
||||
// analysis.push_ignores(ignores);
|
||||
// this.children.forEach((child) => {
|
||||
// child.warn_on_unused_selector((selector) => {
|
||||
// analysis.warn(selector.node, {
|
||||
// code: 'css-unused-selector',
|
||||
// message: `Unused CSS selector "${this.source.slice(
|
||||
// selector.node.start,
|
||||
// selector.node.end
|
||||
// )}"`
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// analysis.pop_ignores();
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
import { error } from '../../../errors.js';
|
||||
import { is_keyframes_node } from '../../css.js';
|
||||
import { merge } from '../../visitors.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('zimmerframe').Visitors<
|
||||
* import('#compiler').Css.Node,
|
||||
* NonNullable<import('../../types.js').ComponentAnalysis['css']>
|
||||
* >} Visitors
|
||||
*/
|
||||
|
||||
/** @param {import('#compiler').Css.RelativeSelector} relative_selector */
|
||||
function is_global(relative_selector) {
|
||||
const first = relative_selector.selectors[0];
|
||||
|
||||
return (
|
||||
first.type === 'PseudoClassSelector' &&
|
||||
first.name === 'global' &&
|
||||
relative_selector.selectors.every(
|
||||
(selector) =>
|
||||
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {Visitors} */
|
||||
const analysis = {
|
||||
Atrule(node, context) {
|
||||
if (is_keyframes_node(node)) {
|
||||
if (!node.prelude.startsWith('-global-')) {
|
||||
context.state.keyframes.push(node.prelude);
|
||||
}
|
||||
}
|
||||
},
|
||||
ComplexSelector(node, context) {
|
||||
context.next(); // analyse relevant selectors first
|
||||
|
||||
node.metadata.used = node.children.every(
|
||||
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
|
||||
);
|
||||
},
|
||||
RelativeSelector(node, context) {
|
||||
node.metadata.is_global =
|
||||
node.selectors.length >= 1 &&
|
||||
node.selectors[0].type === 'PseudoClassSelector' &&
|
||||
node.selectors[0].name === 'global' &&
|
||||
node.selectors.every(
|
||||
(selector) =>
|
||||
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
||||
);
|
||||
|
||||
if (node.selectors.length === 1) {
|
||||
const first = node.selectors[0];
|
||||
node.metadata.is_host = first.type === 'PseudoClassSelector' && first.name === 'host';
|
||||
}
|
||||
|
||||
node.metadata.is_root = !!node.selectors.find(
|
||||
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
|
||||
);
|
||||
|
||||
context.next();
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {Visitors} */
|
||||
const validation = {
|
||||
ComplexSelector(node, context) {
|
||||
// ensure `:global(...)` is not used in the middle of a selector
|
||||
{
|
||||
const a = node.children.findIndex((child) => !is_global(child));
|
||||
const b = node.children.findLastIndex((child) => !is_global(child));
|
||||
|
||||
if (a !== b) {
|
||||
for (let i = a; i <= b; i += 1) {
|
||||
if (is_global(node.children[i])) {
|
||||
error(node.children[i].selectors[0], 'invalid-css-global-placement');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure `:global(...)`contains a single selector
|
||||
// (standalone :global() with multiple selectors is OK)
|
||||
if (node.children.length > 1 || node.children[0].selectors.length > 1) {
|
||||
for (const relative_selector of node.children) {
|
||||
for (const selector of relative_selector.selectors) {
|
||||
if (
|
||||
selector.type === 'PseudoClassSelector' &&
|
||||
selector.name === 'global' &&
|
||||
selector.args !== null &&
|
||||
selector.args.children.length > 1
|
||||
) {
|
||||
error(selector, 'invalid-css-global-selector');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure `:global(...)` is not part of a larger compound selector
|
||||
for (const relative_selector of node.children) {
|
||||
for (let i = 0; i < relative_selector.selectors.length; i++) {
|
||||
const selector = relative_selector.selectors[i];
|
||||
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
const child = selector.args?.children[0].children[0];
|
||||
if (
|
||||
child?.selectors[0].type === 'TypeSelector' &&
|
||||
!/[.:#]/.test(child.selectors[0].name[0]) &&
|
||||
(i !== 0 ||
|
||||
relative_selector.selectors
|
||||
.slice(1)
|
||||
.some(
|
||||
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
|
||||
))
|
||||
) {
|
||||
error(selector, 'invalid-css-global-selector-list');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const css_visitors = merge(analysis, validation);
|
@ -1,18 +1,3 @@
|
||||
const regex_return_characters = /\r/g;
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hash(str) {
|
||||
str = str.replace(regex_return_characters, '');
|
||||
let hash = 5381;
|
||||
let i = str.length;
|
||||
|
||||
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
|
||||
const UNKNOWN = {};
|
||||
|
||||
/**
|
@ -0,0 +1,14 @@
|
||||
const regex_return_characters = /\r/g;
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hash(str) {
|
||||
str = str.replace(regex_return_characters, '');
|
||||
let hash = 5381;
|
||||
let i = str.length;
|
||||
|
||||
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
@ -0,0 +1,252 @@
|
||||
import MagicString from 'magic-string';
|
||||
import { walk } from 'zimmerframe';
|
||||
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
|
||||
import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
|
||||
|
||||
/** @typedef {{ code: MagicString, dev: boolean, hash: string, selector: string, keyframes: string[] }} State */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {import('../../types.js').ComponentAnalysis} analysis
|
||||
* @param {import('#compiler').ValidatedCompileOptions} options
|
||||
*/
|
||||
export function render_stylesheet(source, analysis, options) {
|
||||
const code = new MagicString(source);
|
||||
|
||||
/** @type {State} */
|
||||
const state = {
|
||||
code,
|
||||
dev: options.dev,
|
||||
hash: analysis.css.hash,
|
||||
selector: `.${analysis.css.hash}`,
|
||||
keyframes: analysis.css.keyframes
|
||||
};
|
||||
|
||||
const ast = /** @type {import('#compiler').Css.StyleSheet} */ (analysis.css.ast);
|
||||
|
||||
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, visitors);
|
||||
|
||||
code.remove(0, ast.content.start);
|
||||
code.remove(/** @type {number} */ (ast.content.end), source.length);
|
||||
|
||||
const css = {
|
||||
code: code.toString(),
|
||||
map: code.generateMap({
|
||||
// include source content; makes it easier/more robust looking up the source map code
|
||||
includeContent: true,
|
||||
// generateMap takes care of calculating source relative to file
|
||||
source: options.filename,
|
||||
file: options.cssOutputFilename || options.filename
|
||||
})
|
||||
};
|
||||
|
||||
merge_with_preprocessor_map(css, options, css.map.sources[0]);
|
||||
|
||||
if (options.dev && options.css === 'injected' && css.code) {
|
||||
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
|
||||
const visitors = {
|
||||
_: (node, context) => {
|
||||
context.state.code.addSourcemapLocation(node.start);
|
||||
context.state.code.addSourcemapLocation(node.end);
|
||||
context.next();
|
||||
},
|
||||
Atrule(node, { state, next }) {
|
||||
if (is_keyframes_node(node)) {
|
||||
let start = node.start + node.name.length + 1;
|
||||
while (state.code.original[start] === ' ') start += 1;
|
||||
let end = start;
|
||||
while (state.code.original[end] !== '{' && state.code.original[end] !== ' ') end += 1;
|
||||
|
||||
if (node.prelude.startsWith('-global-')) {
|
||||
state.code.remove(start, start + 8);
|
||||
} else {
|
||||
state.code.prependRight(start, `${state.hash}-`);
|
||||
}
|
||||
|
||||
return; // don't transform anything within
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
Declaration(node, { state, next }) {
|
||||
const property = node.property && remove_css_prefix(node.property.toLowerCase());
|
||||
if (property === 'animation' || property === 'animation-name') {
|
||||
let index = node.start + node.property.length + 1;
|
||||
let name = '';
|
||||
|
||||
while (index < state.code.original.length) {
|
||||
const character = state.code.original[index];
|
||||
|
||||
if (regex_css_name_boundary.test(character)) {
|
||||
if (state.keyframes.includes(name)) {
|
||||
state.code.prependRight(index - name.length, `${state.hash}-`);
|
||||
}
|
||||
|
||||
if (character === ';' || character === '}') {
|
||||
break;
|
||||
}
|
||||
|
||||
name = '';
|
||||
} else {
|
||||
name += character;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
},
|
||||
Rule(node, { state, next }) {
|
||||
// keep empty rules in dev, because it's convenient to
|
||||
// see them in devtools
|
||||
if (!state.dev && is_empty(node)) {
|
||||
state.code.prependRight(node.start, '/* (empty) ');
|
||||
state.code.appendLeft(node.end, '*/');
|
||||
escape_comment_close(node, state.code);
|
||||
return;
|
||||
}
|
||||
|
||||
const used = node.prelude.children.filter((s) => s.metadata.used);
|
||||
|
||||
if (used.length === 0) {
|
||||
state.code.prependRight(node.start, '/* (unused) ');
|
||||
state.code.appendLeft(node.end, '*/');
|
||||
escape_comment_close(node, state.code);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (used.length < node.prelude.children.length) {
|
||||
let pruning = false;
|
||||
let last = node.prelude.children[0].start;
|
||||
|
||||
for (let i = 0; i < node.prelude.children.length; i += 1) {
|
||||
const selector = node.prelude.children[i];
|
||||
|
||||
if (selector.metadata.used === pruning) {
|
||||
if (pruning) {
|
||||
let i = selector.start;
|
||||
while (state.code.original[i] !== ',') i--;
|
||||
|
||||
state.code.overwrite(i, i + 1, '*/');
|
||||
} else {
|
||||
if (i === 0) {
|
||||
state.code.prependRight(selector.start, '/* (unused) ');
|
||||
} else {
|
||||
state.code.overwrite(last, selector.start, ' /* (unused) ');
|
||||
}
|
||||
}
|
||||
|
||||
pruning = !pruning;
|
||||
}
|
||||
|
||||
last = selector.end;
|
||||
}
|
||||
|
||||
if (pruning) {
|
||||
state.code.appendLeft(last, '*/');
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
ComplexSelector(node, context) {
|
||||
/** @param {import('#compiler').Css.SimpleSelector} selector */
|
||||
function remove_global_pseudo_class(selector) {
|
||||
context.state.code
|
||||
.remove(selector.start, selector.start + ':global('.length)
|
||||
.remove(selector.end - 1, selector.end);
|
||||
}
|
||||
|
||||
let first = true;
|
||||
|
||||
for (const relative_selector of node.children) {
|
||||
if (relative_selector.metadata.is_global) {
|
||||
remove_global_pseudo_class(relative_selector.selectors[0]);
|
||||
}
|
||||
|
||||
if (relative_selector.metadata.scoped) {
|
||||
// for the first occurrence, we use a classname selector, so that every
|
||||
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
|
||||
// we use a `:where` selector, which does not affect specificity
|
||||
let modifier = context.state.selector;
|
||||
if (!first) modifier = `:where(${modifier})`;
|
||||
|
||||
first = false;
|
||||
|
||||
// TODO err... can this happen?
|
||||
for (const selector of relative_selector.selectors) {
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
remove_global_pseudo_class(selector);
|
||||
}
|
||||
}
|
||||
|
||||
let i = relative_selector.selectors.length;
|
||||
while (i--) {
|
||||
const selector = relative_selector.selectors[i];
|
||||
|
||||
if (
|
||||
selector.type === 'PseudoElementSelector' ||
|
||||
selector.type === 'PseudoClassSelector'
|
||||
) {
|
||||
if (selector.name !== 'root' && selector.name !== 'host') {
|
||||
if (i === 0) context.state.code.prependRight(selector.start, modifier);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selector.type === 'TypeSelector' && selector.name === '*') {
|
||||
context.state.code.update(selector.start, selector.end, modifier);
|
||||
} else {
|
||||
context.state.code.appendLeft(selector.end, modifier);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {import('#compiler').Css.Rule} rule */
|
||||
function is_empty(rule) {
|
||||
if (rule.block.children.length > 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('#compiler').Css.Rule} node
|
||||
* @param {MagicString} code
|
||||
*/
|
||||
function escape_comment_close(node, code) {
|
||||
let escaped = false;
|
||||
let in_comment = false;
|
||||
|
||||
for (let i = node.start; i < node.end; i++) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else {
|
||||
const char = code.original[i];
|
||||
if (in_comment) {
|
||||
if (char === '*' && code.original[i + 1] === '/') {
|
||||
code.prependRight(++i, '\\');
|
||||
in_comment = false;
|
||||
}
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === '/' && code.original[++i] === '*') {
|
||||
in_comment = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
|
||||
export const regex_css_name_boundary = /^[\s,;}]$/;
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function remove_css_prefix(name) {
|
||||
return name.replace(regex_css_browser_prefix, '');
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.Atrule} node */
|
||||
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
|
@ -1,3 +1,3 @@
|
||||
/* (empty) .foo.svelte-xyz {
|
||||
/* (empty) .foo {
|
||||
/* empty *\/
|
||||
}*/
|
||||
|
@ -1,3 +1,3 @@
|
||||
/* (unused) .foo .bar {
|
||||
/* (unused) :global(.foo) .bar {
|
||||
color: red;
|
||||
}*/
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue