Convert src/compiler/compile/css/Stylesheet.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent 819ca97402
commit d3a410913d

@ -1,22 +1,26 @@
import { gather_possible_values, UNKNOWN } from './gather_possible_values.js'; import { gather_possible_values, UNKNOWN } from './gather_possible_values.js';
import compiler_errors from '../compiler_errors.js'; import compiler_errors from '../compiler_errors.js';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../utils/patterns.js'; import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../utils/patterns.js';
let BlockAppliesToNode;
(function (BlockAppliesToNode) { const BlockAppliesToNode = /** @type {const} */ ({
BlockAppliesToNode[(BlockAppliesToNode['NotPossible'] = 0)] = 'NotPossible'; NotPossible: 0,
BlockAppliesToNode[(BlockAppliesToNode['Possible'] = 1)] = 'Possible'; Possible: 1,
BlockAppliesToNode[(BlockAppliesToNode['UnknownSelectorType'] = 2)] = 'UnknownSelectorType'; UnknownSelectorType: 2
})(BlockAppliesToNode || (BlockAppliesToNode = {})); });
let NodeExist;
(function (NodeExist) { const NodeExist = /** @type {const} */ ({
NodeExist[(NodeExist['Probably'] = 1)] = 'Probably'; Probably: 0,
NodeExist[(NodeExist['Definitely'] = 2)] = 'Definitely'; Definitely: 1
})(NodeExist || (NodeExist = {})); });
/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */
const whitelist_attribute_selector = new Map([ const whitelist_attribute_selector = new Map([
['details', new Set(['open'])], ['details', new Set(['open'])],
['dialog', new Set(['open'])] ['dialog', new Set(['open'])]
]); ]);
const regex_is_single_css_selector = /[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/; const regex_is_single_css_selector = /[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/;
export default class Selector { export default class Selector {
/** @type {import('./private.js').CssNode} */ /** @type {import('./private.js').CssNode} */
node; node;
@ -217,6 +221,7 @@ export default class Selector {
} }
} }
} }
get_amount_class_specificity_increased() { get_amount_class_specificity_increased() {
let count = 0; let count = 0;
for (const block of this.blocks) { for (const block of this.blocks) {
@ -314,12 +319,13 @@ function apply_selector(blocks, node, to_encapsulate) {
to_encapsulate.push({ node, block }); to_encapsulate.push({ node, block });
return true; return true;
} }
const regex_backslash_and_following_character = /\\(.)/g; const regex_backslash_and_following_character = /\\(.)/g;
/** /**
* @param {Block} block * @param {Block} block
* @param {import('../nodes/Element.js').default} node * @param {import('../nodes/Element.js').default} node
* @returns {BlockAppliesToNode} * @returns {typeof BlockAppliesToNode[keyof typeof BlockAppliesToNode]}
*/ */
function block_might_apply_to_node(block, node) { function block_might_apply_to_node(block, node) {
let i = block.selectors.length; let i = block.selectors.length;
@ -519,6 +525,7 @@ function get_element_parent(node) {
while ((parent = parent.parent) && parent.type !== 'Element'); while ((parent = parent.parent) && parent.type !== 'Element');
return /** @type {import('../nodes/Element.js').default | null} */ (parent); return /** @type {import('../nodes/Element.js').default | null} */ (parent);
} }
/** /**
* Finds the given node's previous sibling in the DOM * Finds the given node's previous sibling in the DOM
* *
@ -559,10 +566,10 @@ function find_previous_sibling(node) {
/** /**
* @param {import('../nodes/interfaces.js').INode} node * @param {import('../nodes/interfaces.js').INode} node
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @returns {Map<import('../nodes/Element.js').default, NodeExist>} * @returns {Map<import('../nodes/Element.js').default, NodeExistsValue>}
*/ */
function get_possible_element_siblings(node, adjacent_only) { function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {Map<import('../nodes/Element.js').default, NodeExistsValue>} */
const result = new Map(); const result = new Map();
/** @type {import('../nodes/interfaces.js').INode} */ /** @type {import('../nodes/interfaces.js').INode} */
@ -622,16 +629,18 @@ function get_possible_element_siblings(node, adjacent_only) {
/** /**
* @param {import('../nodes/EachBlock.js').default | import('../nodes/IfBlock.js').default | import('../nodes/AwaitBlock.js').default} block * @param {import('../nodes/EachBlock.js').default | import('../nodes/IfBlock.js').default | import('../nodes/AwaitBlock.js').default} block
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @returns {Map<import('../nodes/Element.js').default, NodeExist>} * @returns {Map<import('../nodes/Element.js').default, NodeExistsValue>}
*/ */
function get_possible_last_child(block, adjacent_only) { function get_possible_last_child(block, adjacent_only) {
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @typedef {Map<import('../nodes/Element.js').default, NodeExistsValue>} NodeMap */
/** @type {NodeMap} */
const result = new Map(); const result = new Map();
if (block.type === 'EachBlock') { if (block.type === 'EachBlock') {
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const each_result = loop_child(block.children, adjacent_only); const each_result = loop_child(block.children, adjacent_only);
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const else_result = block.else ? loop_child(block.else.children, adjacent_only) : new Map(); const else_result = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(else_result); const not_exhaustive = !has_definite_elements(else_result);
if (not_exhaustive) { if (not_exhaustive) {
@ -641,10 +650,10 @@ function get_possible_last_child(block, adjacent_only) {
add_to_map(each_result, result); add_to_map(each_result, result);
add_to_map(else_result, result); add_to_map(else_result, result);
} else if (block.type === 'IfBlock') { } else if (block.type === 'IfBlock') {
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const if_result = loop_child(block.children, adjacent_only); const if_result = loop_child(block.children, adjacent_only);
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const else_result = block.else ? loop_child(block.else.children, adjacent_only) : new Map(); const else_result = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result); const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
if (not_exhaustive) { if (not_exhaustive) {
@ -654,15 +663,15 @@ function get_possible_last_child(block, adjacent_only) {
add_to_map(if_result, result); add_to_map(if_result, result);
add_to_map(else_result, result); add_to_map(else_result, result);
} else if (block.type === 'AwaitBlock') { } else if (block.type === 'AwaitBlock') {
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const pending_result = block.pending const pending_result = block.pending
? loop_child(block.pending.children, adjacent_only) ? loop_child(block.pending.children, adjacent_only)
: new Map(); : new Map();
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const then_result = block.then ? loop_child(block.then.children, adjacent_only) : new Map(); const then_result = block.then ? loop_child(block.then.children, adjacent_only) : new Map();
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {NodeMap} */
const catch_result = block.catch ? loop_child(block.catch.children, adjacent_only) : new Map(); const catch_result = block.catch ? loop_child(block.catch.children, adjacent_only) : new Map();
const not_exhaustive = const not_exhaustive =
!has_definite_elements(pending_result) || !has_definite_elements(pending_result) ||
@ -681,7 +690,7 @@ function get_possible_last_child(block, adjacent_only) {
} }
/** /**
* @param {Map<import('../nodes/Element.js').default, NodeExist>} result * @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} result
* @returns {boolean} * @returns {boolean}
*/ */
function has_definite_elements(result) { function has_definite_elements(result) {
@ -695,8 +704,9 @@ function has_definite_elements(result) {
} }
/** /**
* @param {Map<import('../nodes/Element.js').default, NodeExist>} from * @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} from
* @param {Map<import('../nodes/Element.js').default, NodeExist>} to * @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} to
* @returns {void}
*/ */
function add_to_map(from, to) { function add_to_map(from, to) {
from.forEach((exist, element) => { from.forEach((exist, element) => {
@ -705,16 +715,16 @@ function add_to_map(from, to) {
} }
/** /**
* @param {NodeExist | null} exist1 * @param {NodeExistsValue | null} exist1
* @param {NodeExist | null} exist2 * @param {NodeExistsValue | null} exist2
* @returns {NodeExist} * @returns {NodeExistsValue}
*/ */
function higher_existence(exist1, exist2) { function higher_existence(exist1, exist2) {
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2; if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2; return exist1 > exist2 ? exist1 : exist2;
} }
/** @param {Map<import('../nodes/Element.js').default, NodeExist>} result */ /** @param {Map<import('../nodes/Element.js').default, NodeExistsValue>} result */
function mark_as_probably(result) { function mark_as_probably(result) {
for (const key of result.keys()) { for (const key of result.keys()) {
result.set(key, NodeExist.Probably); result.set(key, NodeExist.Probably);
@ -726,7 +736,7 @@ function mark_as_probably(result) {
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
*/ */
function loop_child(children, adjacent_only) { function loop_child(children, adjacent_only) {
/** @type {Map<import('../nodes/Element.js').default, NodeExist>} */ /** @type {Map<import('../nodes/Element.js').default, NodeExistsValue>} */
const result = new Map(); const result = new Map();
for (let i = children.length - 1; i >= 0; i--) { for (let i = children.length - 1; i >= 0; i--) {
const child = children[i]; const child = children[i];
@ -749,6 +759,7 @@ function loop_child(children, adjacent_only) {
} }
return result; return result;
} }
class Block { class Block {
/** @type {boolean} */ /** @type {boolean} */
host; host;

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

Loading…
Cancel
Save