Lint and format src/compiler/compile/css/Stylesheet.js

pull/8569/head
Simon Holthausen 3 years ago
parent 205993151e
commit 61ca425898

@ -13,7 +13,7 @@ const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
* @returns {string} * @returns {string}
*/ */
function remove_css_prefix(name) { function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, ''); return name.replace(regex_css_browser_prefix, '');
} }
/** @param {import('./private.js').CssNode} node */ /** @param {import('./private.js').CssNode} node */
@ -23,7 +23,8 @@ const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes'
* @param {import('./private.js').CssNode} * @param {import('./private.js').CssNode}
* @returns {true} * @returns {true}
*/ */
const at_rule_has_declaration = ({ block }) => block && block.children && block.children.find((node) => node.type === 'Declaration'); const at_rule_has_declaration = ({ block }) =>
block && block.children && block.children.find((node) => node.type === 'Declaration');
/** /**
* @param {import('magic-string').default} code * @param {import('magic-string').default} code
@ -32,492 +33,480 @@ const at_rule_has_declaration = ({ block }) => block && block.children && block.
* @returns {number} * @returns {number}
*/ */
function minify_declarations(code, start, declarations) { function minify_declarations(code, start, declarations) {
let c = start; let c = start;
declarations.forEach((declaration, i) => { declarations.forEach((declaration, i) => {
const separator = i > 0 ? ';' : ''; const separator = i > 0 ? ';' : '';
if (declaration.node.start - c > separator.length) { if (declaration.node.start - c > separator.length) {
code.update(c, declaration.node.start, separator); code.update(c, declaration.node.start, separator);
} }
declaration.minify(code); declaration.minify(code);
c = declaration.node.end; c = declaration.node.end;
}); });
return c; return c;
} }
class Rule { class Rule {
/** @type {import('./Selector.js').default[]} */
/** @type {import('./Selector.js').default[]} */ selectors;
selectors;
/** @type {Declaration[]} */
/** @type {Declaration[]} */ declarations;
declarations;
/** @type {import('./private.js').CssNode} */
/** @type {import('./private.js').CssNode} */ node;
node;
/** @type {Atrule} */
/** @type {Atrule} */ parent;
parent;
/**
/** * @param {import('./private.js').CssNode} node
* @param {import('./private.js').CssNode} node * @param {any} stylesheet
* @param {any} stylesheet * @param {Atrule} [parent]
* @param {Atrule} [parent] */
*/ constructor(node, stylesheet, parent) {
constructor(node, stylesheet, parent) { this.node = node;
this.node = node; this.parent = parent;
this.parent = parent; this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet));
this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet)); this.declarations = node.block.children.map((node) => new Declaration(node));
this.declarations = node.block.children.map((node) => new Declaration(node)); }
}
/** @param {import('../nodes/Element.js').default} node */
/** @param {import('../nodes/Element.js').default} node */ apply(node) {
apply(node) { 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 */
/** @param {boolean} dev */ is_used(dev) {
is_used(dev) { if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node))
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) return true;
return true; if (this.declarations.length === 0) return dev;
if (this.declarations.length === 0) return this.selectors.some((s) => s.used);
return dev; }
return this.selectors.some((s) => s.used);
} /**
* @param {import('magic-string').default} code
/** * @param {boolean} _dev
* @param {import('magic-string').default} code */
* @param {boolean} _dev minify(code, _dev) {
*/ let c = this.node.start;
minify(code, _dev) { let started = false;
let c = this.node.start; this.selectors.forEach((selector) => {
let started = false; if (selector.used) {
this.selectors.forEach((selector) => { const separator = started ? ',' : '';
if (selector.used) { if (selector.node.start - c > separator.length) {
const separator = started ? ',' : ''; code.update(c, selector.node.start, separator);
if (selector.node.start - c > separator.length) { }
code.update(c, selector.node.start, separator); selector.minify(code);
} c = selector.node.end;
selector.minify(code); started = true;
c = selector.node.end; }
started = true; });
} code.remove(c, this.node.block.start);
}); c = this.node.block.start + 1;
code.remove(c, this.node.block.start); c = minify_declarations(code, c, this.declarations);
c = this.node.block.start + 1; code.remove(c, this.node.block.end - 1);
c = minify_declarations(code, c, this.declarations); }
code.remove(c, this.node.block.end - 1);
} /**
* @param {import('magic-string').default} code
/** * @param {string} id
* @param {import('magic-string').default} code * @param {Map<string, string>} keyframes
* @param {string} id * @param {number} max_amount_class_specificity_increased
* @param {Map<string, string>} keyframes */
* @param {number} max_amount_class_specificity_increased transform(code, id, keyframes, max_amount_class_specificity_increased) {
*/ if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node))
transform(code, id, keyframes, max_amount_class_specificity_increased) { return true;
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) const attr = `.${id}`;
return true; this.selectors.forEach((selector) =>
const attr = `.${id}`; selector.transform(code, attr, max_amount_class_specificity_increased)
this.selectors.forEach((selector) => selector.transform(code, attr, max_amount_class_specificity_increased)); );
this.declarations.forEach((declaration) => declaration.transform(code, keyframes)); this.declarations.forEach((declaration) => declaration.transform(code, keyframes));
} }
/** @param {import('../Component.js').default} component */ /** @param {import('../Component.js').default} component */
validate(component) { validate(component) {
this.selectors.forEach((selector) => { this.selectors.forEach((selector) => {
selector.validate(component); selector.validate(component);
}); });
} }
/** @param {(selector: import('./Selector.js').default) => void} handler */ /** @param {(selector: import('./Selector.js').default) => void} handler */
warn_on_unused_selector(handler) { warn_on_unused_selector(handler) {
this.selectors.forEach((selector) => { this.selectors.forEach((selector) => {
if (!selector.used) if (!selector.used) handler(selector);
handler(selector); });
}); }
} get_max_amount_class_specificity_increased() {
get_max_amount_class_specificity_increased() { return Math.max(
return Math.max(...this.selectors.map((selector) => selector.get_amount_class_specificity_increased())); ...this.selectors.map((selector) => selector.get_amount_class_specificity_increased())
} );
}
} }
class Declaration { class Declaration {
/** @type {import('./private.js').CssNode} */
/** @type {import('./private.js').CssNode} */ node;
node;
/** @param {import('./private.js').CssNode} node */
/** @param {import('./private.js').CssNode} node */ constructor(node) {
constructor(node) { this.node = node;
this.node = node; }
}
/**
/** * @param {import('magic-string').default} code
* @param {import('magic-string').default} code * @param {Map<string, string>} keyframes
* @param {Map<string, string>} keyframes */
*/ transform(code, keyframes) {
transform(code, keyframes) { const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase()); if (property === 'animation' || property === 'animation-name') {
if (property === 'animation' || property === 'animation-name') { this.node.value.children.forEach((block) => {
this.node.value.children.forEach((block) => { if (block.type === 'Identifier') {
if (block.type === 'Identifier') { const name = block.name;
const name = block.name; if (keyframes.has(name)) {
if (keyframes.has(name)) { code.update(block.start, block.end, keyframes.get(name));
code.update(block.start, block.end, keyframes.get(name)); }
} }
} });
}); }
} }
}
/** @param {import('magic-string').default} code */
/** @param {import('magic-string').default} code */ minify(code) {
minify(code) { if (!this.node.property) return; // @apply, and possibly other weird cases?
if (!this.node.property) const c = this.node.start + this.node.property.length;
return; // @apply, and possibly other weird cases? const first = this.node.value.children ? this.node.value.children[0] : this.node.value;
const c = this.node.start + this.node.property.length; // Don't minify whitespace in custom properties, since some browsers (Chromium < 99)
const first = this.node.value.children ? this.node.value.children[0] : this.node.value; // treat --foo: ; and --foo:; differently
// Don't minify whitespace in custom properties, since some browsers (Chromium < 99) if (first.type === 'Raw' && regex_only_whitespaces.test(first.value)) return;
// treat --foo: ; and --foo:; differently let start = first.start;
if (first.type === 'Raw' && regex_only_whitespaces.test(first.value)) while (regex_whitespace.test(code.original[start])) start += 1;
return; if (start - c > 1) {
let start = first.start; code.update(c, start, ':');
while (regex_whitespace.test(code.original[start])) }
start += 1; }
if (start - c > 1) {
code.update(c, start, ':');
}
}
} }
class Atrule { class Atrule {
/** @type {import('./private.js').CssNode} */
/** @type {import('./private.js').CssNode} */ node;
node;
/** @type {Array<Atrule | Rule>} */
/** @type {Array<Atrule | Rule>} */ children;
children;
/** @type {Declaration[]} */
/** @type {Declaration[]} */ declarations;
declarations;
/** @param {import('./private.js').CssNode} node */
/** @param {import('./private.js').CssNode} node */ constructor(node) {
constructor(node) { this.node = node;
this.node = node; this.children = [];
this.children = []; this.declarations = [];
this.declarations = []; }
}
/** @param {import('../nodes/Element.js').default} node */
/** @param {import('../nodes/Element.js').default} node */ apply(node) {
apply(node) { if (
if (this.node.name === 'container' || this.node.name === 'container' ||
this.node.name === 'media' || this.node.name === 'media' ||
this.node.name === 'supports' || this.node.name === 'supports' ||
this.node.name === 'layer') { this.node.name === 'layer'
this.children.forEach((child) => { ) {
child.apply(node); this.children.forEach((child) => {
}); child.apply(node);
} });
else if (is_keyframes_node(this.node)) { } else if (is_keyframes_node(this.node)) {
this.children.forEach((rule) => { this.children.forEach((rule) => {
rule.selectors.forEach((selector) => { rule.selectors.forEach((selector) => {
selector.used = true; selector.used = true;
}); });
}); });
} }
} }
/** @param {boolean} _dev */ /** @param {boolean} _dev */
is_used(_dev) { is_used(_dev) {
return true; // TODO return true; // TODO
} }
/** /**
* @param {import('magic-string').default} code * @param {import('magic-string').default} code
* @param {boolean} dev * @param {boolean} dev
*/ */
minify(code, dev) { minify(code, dev) {
if (this.node.name === 'media') { if (this.node.name === 'media') {
const expression_char = code.original[this.node.prelude.start]; const expression_char = code.original[this.node.prelude.start];
let c = this.node.start + (expression_char === '(' ? 6 : 7); let c = this.node.start + (expression_char === '(' ? 6 : 7);
if (this.node.prelude.start > c) if (this.node.prelude.start > c) code.remove(c, this.node.prelude.start);
code.remove(c, this.node.prelude.start); this.node.prelude.children.forEach((query) => {
this.node.prelude.children.forEach((query) => { // TODO minify queries
// TODO minify queries c = query.end;
c = query.end; });
}); code.remove(c, this.node.block.start);
code.remove(c, this.node.block.start); } else if (this.node.name === 'supports') {
} let c = this.node.start + 9;
else if (this.node.name === 'supports') { if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
let c = this.node.start + 9; this.node.prelude.children.forEach((query) => {
if (this.node.prelude.start - c > 1) // TODO minify queries
code.update(c, this.node.prelude.start, ' '); c = query.end;
this.node.prelude.children.forEach((query) => { });
// TODO minify queries code.remove(c, this.node.block.start);
c = query.end; } else {
}); let c = this.node.start + this.node.name.length + 1;
code.remove(c, this.node.block.start); if (this.node.prelude) {
} if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' ');
else { c = this.node.prelude.end;
let c = this.node.start + this.node.name.length + 1; }
if (this.node.prelude) { if (this.node.block && this.node.block.start - c > 0) {
if (this.node.prelude.start - c > 1) code.remove(c, this.node.block.start);
code.update(c, this.node.prelude.start, ' '); }
c = this.node.prelude.end; }
} // TODO other atrules
if (this.node.block && this.node.block.start - c > 0) { if (this.node.block) {
code.remove(c, this.node.block.start); let c = this.node.block.start + 1;
} if (this.declarations.length) {
} c = minify_declarations(code, c, this.declarations);
// TODO other atrules // if the atrule has children, leave the last declaration semicolon alone
if (this.node.block) { if (this.children.length) c++;
let c = this.node.block.start + 1; }
if (this.declarations.length) { this.children.forEach((child) => {
c = minify_declarations(code, c, this.declarations); if (child.is_used(dev)) {
// if the atrule has children, leave the last declaration semicolon alone code.remove(c, child.node.start);
if (this.children.length) child.minify(code, dev);
c++; c = child.node.end;
} }
this.children.forEach((child) => { });
if (child.is_used(dev)) { code.remove(c, this.node.block.end - 1);
code.remove(c, child.node.start); }
child.minify(code, dev); }
c = child.node.end;
} /**
}); * @param {import('magic-string').default} code
code.remove(c, this.node.block.end - 1); * @param {string} id
} * @param {Map<string, string>} keyframes
} * @param {number} max_amount_class_specificity_increased
*/
/** transform(code, id, keyframes, max_amount_class_specificity_increased) {
* @param {import('magic-string').default} code if (is_keyframes_node(this.node)) {
* @param {string} id this.node.prelude.children.forEach(({ type, name, start, end }) => {
* @param {Map<string, string>} keyframes if (type === 'Identifier') {
* @param {number} max_amount_class_specificity_increased if (name.startsWith('-global-')) {
*/ code.remove(start, start + 8);
transform(code, id, keyframes, max_amount_class_specificity_increased) { this.children.forEach((rule) => {
if (is_keyframes_node(this.node)) { rule.selectors.forEach((selector) => {
this.node.prelude.children.forEach(({ type, name, start, end }) => { selector.used = true;
if (type === 'Identifier') { });
if (name.startsWith('-global-')) { });
code.remove(start, start + 8); } else {
this.children.forEach((rule) => { code.update(start, end, keyframes.get(name));
rule.selectors.forEach((selector) => { }
selector.used = true; }
}); });
}); }
} this.children.forEach((child) => {
else { child.transform(code, id, keyframes, max_amount_class_specificity_increased);
code.update(start, end, keyframes.get(name)); });
} }
}
}); /** @param {import('../Component.js').default} component */
} validate(component) {
this.children.forEach((child) => { this.children.forEach((child) => {
child.transform(code, id, keyframes, max_amount_class_specificity_increased); child.validate(component);
}); });
} }
/** @param {import('../Component.js').default} component */ /** @param {(selector: import('./Selector.js').default) => void} handler */
validate(component) { warn_on_unused_selector(handler) {
this.children.forEach((child) => { if (this.node.name !== 'media') return;
child.validate(component); this.children.forEach((child) => {
}); child.warn_on_unused_selector(handler);
} });
}
/** @param {(selector: import('./Selector.js').default) => void} handler */ get_max_amount_class_specificity_increased() {
warn_on_unused_selector(handler) { return Math.max(
if (this.node.name !== 'media') ...this.children.map((rule) => rule.get_max_amount_class_specificity_increased())
return; );
this.children.forEach((child) => { }
child.warn_on_unused_selector(handler);
});
}
get_max_amount_class_specificity_increased() {
return Math.max(...this.children.map((rule) => rule.get_max_amount_class_specificity_increased()));
}
} }
/** @param {any} */ /** @param {any} */
const get_default_css_hash = ({ css, hash }) => { const get_default_css_hash = ({ css, hash }) => {
return `svelte-${hash(css)}`; return `svelte-${hash(css)}`;
}; };
export default class Stylesheet { export default class Stylesheet {
/** @type {string} */
/** @type {string} */ source;
source;
/** @type {import('../../interfaces.js').Ast} */
/** @type {import('../../interfaces.js').Ast} */ ast;
ast;
/** @type {string} */
/** @type {string} */ filename;
filename;
/** @type {boolean} */
/** @type {boolean} */ dev;
dev;
/** @type {boolean} */
/** @type {boolean} */ has_styles;
has_styles;
/** @type {string} */
/** @type {string} */ id;
id;
/** @type {Array<Rule | Atrule>} */
/** @type {Array<Rule | Atrule>} */ children = [];
children = [];
/** @type {Map<string, string>} */
/** @type {Map<string, string>} */ keyframes = new Map();
keyframes = new Map();
/** @type {Set<import('./private.js').CssNode>} */
/** @type {Set<import('./private.js').CssNode>} */ nodes_with_css_class = new Set();
nodes_with_css_class = new Set(); /**
/** * @param {{
* @param {{ * source: string;
* source: string; * ast: import('../../interfaces.js').Ast;
* ast: import('../../interfaces.js').Ast; * filename: string | undefined;
* filename: string | undefined; * component_name: string | undefined;
* component_name: string | undefined; * dev: boolean;
* dev: boolean; * get_css_hash: import('../../interfaces.js').CssHashGetter;
* get_css_hash: import('../../interfaces.js').CssHashGetter; * }}
* }} */
*/ constructor({ source, ast, component_name, filename, dev, get_css_hash = get_default_css_hash }) {
constructor({ source, ast, component_name, filename, dev, get_css_hash = get_default_css_hash }) { this.source = source;
this.source = source; this.ast = ast;
this.ast = ast; this.filename = filename;
this.filename = filename; this.dev = dev;
this.dev = dev; if (ast.css && ast.css.children.length) {
if (ast.css && ast.css.children.length) { this.id = get_css_hash({
this.id = get_css_hash({ filename,
filename, name: component_name,
name: component_name, css: ast.css.content.styles,
css: ast.css.content.styles, hash
hash });
}); this.has_styles = true;
this.has_styles = true;
/** @type {Atrule[]} */
/** @type {Atrule[]} */ const stack = [];
const stack = []; let depth = 0;
let depth = 0;
/** @type {Atrule} */
/** @type {Atrule} */ let current_atrule = null;
let current_atrule = null; walk(/** @type {any} */ (ast.css), {
walk(/** @type {any} */ (ast.css), { enter: (node) => {
enter: (node) => { if (node.type === 'Atrule') {
if (node.type === 'Atrule') { const atrule = new Atrule(node);
const atrule = new Atrule(node); stack.push(atrule);
stack.push(atrule); if (current_atrule) {
if (current_atrule) { current_atrule.children.push(atrule);
current_atrule.children.push(atrule); } else if (depth <= 1) {
} this.children.push(atrule);
else if (depth <= 1) { }
this.children.push(atrule); if (is_keyframes_node(node)) {
} node.prelude.children.forEach((expression) => {
if (is_keyframes_node(node)) { if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) {
node.prelude.children.forEach((expression) => { this.keyframes.set(expression.name, `${this.id}-${expression.name}`);
if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) { }
this.keyframes.set(expression.name, `${this.id}-${expression.name}`); });
} } else if (at_rule_has_declaration(node)) {
}); const at_rule_declarations = node.block.children
} .filter((node) => node.type === 'Declaration')
else if (at_rule_has_declaration(node)) { .map((node) => new Declaration(node));
const at_rule_declarations = node.block.children push_array(atrule.declarations, at_rule_declarations);
.filter((node) => node.type === 'Declaration') }
.map((node) => new Declaration(node)); current_atrule = atrule;
push_array(atrule.declarations, at_rule_declarations); }
} if (node.type === 'Rule') {
current_atrule = atrule; const rule = new Rule(node, this, current_atrule);
} if (current_atrule) {
if (node.type === 'Rule') { current_atrule.children.push(rule);
const rule = new Rule(node, this, current_atrule); } else if (depth <= 1) {
if (current_atrule) { this.children.push(rule);
current_atrule.children.push(rule); }
} }
else if (depth <= 1) { depth += 1;
this.children.push(rule); },
} leave: (node) => {
} if (node.type === 'Atrule') {
depth += 1; stack.pop();
}, current_atrule = stack[stack.length - 1];
leave: (node) => { }
if (node.type === 'Atrule') { depth -= 1;
stack.pop(); }
current_atrule = stack[stack.length - 1]; });
} } else {
depth -= 1; this.has_styles = false;
} }
}); }
}
else { /** @param {import('../nodes/Element.js').default} node */
this.has_styles = false; apply(node) {
} if (!this.has_styles) return;
} for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
/** @param {import('../nodes/Element.js').default} node */ child.apply(node);
apply(node) { }
if (!this.has_styles) }
return; reify() {
for (let i = 0; i < this.children.length; i += 1) { this.nodes_with_css_class.forEach((node) => {
const child = this.children[i]; node.add_css_class();
child.apply(node); });
} }
}
reify() { /** @param {string} file */
this.nodes_with_css_class.forEach((node) => { render(file) {
node.add_css_class(); if (!this.has_styles) {
}); return { code: null, map: null };
} }
const code = new MagicString(this.source);
/** @param {string} file */ walk(/** @type {any} */ (this.ast.css), {
render(file) { enter: (node) => {
if (!this.has_styles) { code.addSourcemapLocation(node.start);
return { code: null, map: null }; code.addSourcemapLocation(node.end);
} }
const code = new MagicString(this.source); });
walk(/** @type {any} */ (this.ast.css), { const max = Math.max(
enter: (node) => { ...this.children.map((rule) => rule.get_max_amount_class_specificity_increased())
code.addSourcemapLocation(node.start); );
code.addSourcemapLocation(node.end); this.children.forEach((child) => {
} child.transform(code, this.id, this.keyframes, max);
}); });
const max = Math.max(...this.children.map((rule) => rule.get_max_amount_class_specificity_increased())); let c = 0;
this.children.forEach((child) => { this.children.forEach((child) => {
child.transform(code, this.id, this.keyframes, max); if (child.is_used(this.dev)) {
}); code.remove(c, child.node.start);
let c = 0; child.minify(code, this.dev);
this.children.forEach((child) => { c = child.node.end;
if (child.is_used(this.dev)) { }
code.remove(c, child.node.start); });
child.minify(code, this.dev); code.remove(c, this.source.length);
c = child.node.end; return {
} code: code.toString(),
}); map: code.generateMap({
code.remove(c, this.source.length); includeContent: true,
return { source: this.filename,
code: code.toString(), file
map: code.generateMap({ })
includeContent: true, };
source: this.filename, }
file
}) /** @param {import('../Component.js').default} component */
}; validate(component) {
} this.children.forEach((child) => {
child.validate(component);
/** @param {import('../Component.js').default} component */ });
validate(component) { }
this.children.forEach((child) => {
child.validate(component); /** @param {import('../Component.js').default} component */
}); warn_on_unused_selectors(component) {
} const ignores = !this.ast.css
? []
/** @param {import('../Component.js').default} component */ : extract_ignores_above_position(this.ast.css.start, this.ast.html.children);
warn_on_unused_selectors(component) { component.push_ignores(ignores);
const ignores = !this.ast.css this.children.forEach((child) => {
? [] child.warn_on_unused_selector((selector) => {
: extract_ignores_above_position(this.ast.css.start, this.ast.html.children); component.warn(
component.push_ignores(ignores); selector.node,
this.children.forEach((child) => { compiler_warnings.css_unused_selector(
child.warn_on_unused_selector((selector) => { this.source.slice(selector.node.start, selector.node.end)
component.warn(selector.node, compiler_warnings.css_unused_selector(this.source.slice(selector.node.start, selector.node.end))); )
}); );
}); });
component.pop_ignores(); });
} component.pop_ignores();
}
} }

Loading…
Cancel
Save