|
import MagicString from 'magic-string'; |
|
import { walk } from 'estree-walker'; |
|
import Selector from './Selector.js'; |
|
import hash from '../utils/hash.js'; |
|
import compiler_warnings from '../compiler_warnings.js'; |
|
import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore.js'; |
|
import { push_array } from '../../utils/push_array.js'; |
|
import { regex_only_whitespaces, regex_whitespace } from '../../utils/patterns.js'; |
|
|
|
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/; |
|
|
|
|
|
|
|
|
|
|
|
function remove_css_prefix(name) { |
|
return name.replace(regex_css_browser_prefix, ''); |
|
} |
|
|
|
|
|
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes'; |
|
|
|
|
|
|
|
|
|
|
|
const at_rule_has_declaration = ({ block }) => |
|
block && block.children && block.children.find((node) => node.type === 'Declaration'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function minify_declarations(code, start, declarations) { |
|
let c = start; |
|
declarations.forEach((declaration, i) => { |
|
const separator = i > 0 ? ';' : ''; |
|
if (declaration.node.start - c > separator.length) { |
|
code.update(c, declaration.node.start, separator); |
|
} |
|
declaration.minify(code); |
|
c = declaration.node.end; |
|
}); |
|
return c; |
|
} |
|
class Rule { |
|
|
|
selectors; |
|
|
|
|
|
declarations; |
|
|
|
|
|
node; |
|
|
|
|
|
parent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(node, stylesheet, parent) { |
|
this.node = node; |
|
this.parent = parent; |
|
this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet)); |
|
this.declarations = node.block.children.map((node) => new Declaration(node)); |
|
} |
|
|
|
|
|
apply(node) { |
|
this.selectors.forEach((selector) => selector.apply(node)); |
|
} |
|
|
|
|
|
is_used(dev) { |
|
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, _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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)); |
|
} |
|
|
|
|
|
validate(component) { |
|
this.selectors.forEach((selector) => { |
|
selector.validate(component); |
|
}); |
|
} |
|
|
|
|
|
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 { |
|
|
|
node; |
|
|
|
|
|
constructor(node) { |
|
this.node = node; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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)); |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
minify(code) { |
|
if (!this.node.property) return; |
|
const c = this.node.start + this.node.property.length; |
|
const first = this.node.value.children ? this.node.value.children[0] : this.node.value; |
|
|
|
|
|
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 { |
|
|
|
node; |
|
|
|
|
|
children; |
|
|
|
|
|
declarations; |
|
|
|
|
|
constructor(node) { |
|
this.node = node; |
|
this.children = []; |
|
this.declarations = []; |
|
} |
|
|
|
|
|
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)) { |
|
this.children.forEach(( rule) => { |
|
rule.selectors.forEach((selector) => { |
|
selector.used = true; |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
|
|
is_used(_dev) { |
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
minify(code, dev) { |
|
if (this.node.name === 'media') { |
|
const expression_char = code.original[this.node.prelude.start]; |
|
let c = this.node.start + (expression_char === '(' ? 6 : 7); |
|
if (this.node.prelude.start > c) code.remove(c, this.node.prelude.start); |
|
this.node.prelude.children.forEach((query) => { |
|
|
|
c = query.end; |
|
}); |
|
code.remove(c, this.node.block.start); |
|
} else if (this.node.name === 'supports') { |
|
let c = this.node.start + 9; |
|
if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' '); |
|
this.node.prelude.children.forEach((query) => { |
|
|
|
c = query.end; |
|
}); |
|
code.remove(c, this.node.block.start); |
|
} else { |
|
let c = this.node.start + this.node.name.length + 1; |
|
if (this.node.prelude) { |
|
if (this.node.prelude.start - c > 1) code.update(c, this.node.prelude.start, ' '); |
|
c = this.node.prelude.end; |
|
} |
|
if (this.node.block && this.node.block.start - c > 0) { |
|
code.remove(c, this.node.block.start); |
|
} |
|
} |
|
|
|
if (this.node.block) { |
|
let c = this.node.block.start + 1; |
|
if (this.declarations.length) { |
|
c = minify_declarations(code, c, this.declarations); |
|
|
|
if (this.children.length) c++; |
|
} |
|
this.children.forEach((child) => { |
|
if (child.is_used(dev)) { |
|
code.remove(c, child.node.start); |
|
child.minify(code, dev); |
|
c = child.node.end; |
|
} |
|
}); |
|
code.remove(c, this.node.block.end - 1); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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-')) { |
|
code.remove(start, start + 8); |
|
this.children.forEach(( rule) => { |
|
rule.selectors.forEach((selector) => { |
|
selector.used = true; |
|
}); |
|
}); |
|
} else { |
|
code.update(start, end, keyframes.get(name)); |
|
} |
|
} |
|
}); |
|
} |
|
this.children.forEach((child) => { |
|
child.transform(code, id, keyframes, max_amount_class_specificity_increased); |
|
}); |
|
} |
|
|
|
|
|
validate(component) { |
|
this.children.forEach((child) => { |
|
child.validate(component); |
|
}); |
|
} |
|
|
|
|
|
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 = ({ css, hash }) => { |
|
return `svelte-${hash(css)}`; |
|
}; |
|
export default class Stylesheet { |
|
|
|
source; |
|
|
|
|
|
ast; |
|
|
|
|
|
filename; |
|
|
|
|
|
dev; |
|
|
|
|
|
has_styles; |
|
|
|
|
|
id; |
|
|
|
|
|
children = []; |
|
|
|
|
|
keyframes = new Map(); |
|
|
|
|
|
nodes_with_css_class = new Set(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor({ source, ast, component_name, filename, dev, get_css_hash = get_default_css_hash }) { |
|
this.source = source; |
|
this.ast = ast; |
|
this.filename = filename; |
|
this.dev = dev; |
|
if (ast.css && ast.css.children.length) { |
|
this.id = get_css_hash({ |
|
filename, |
|
name: component_name, |
|
css: ast.css.content.styles, |
|
hash |
|
}); |
|
this.has_styles = true; |
|
|
|
|
|
const stack = []; |
|
let depth = 0; |
|
|
|
|
|
let current_atrule = null; |
|
walk( (ast.css), { |
|
enter: ( node) => { |
|
if (node.type === 'Atrule') { |
|
const atrule = new Atrule(node); |
|
stack.push(atrule); |
|
if (current_atrule) { |
|
current_atrule.children.push(atrule); |
|
} else if (depth <= 1) { |
|
this.children.push(atrule); |
|
} |
|
if (is_keyframes_node(node)) { |
|
node.prelude.children.forEach((expression) => { |
|
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') |
|
.map((node) => new Declaration(node)); |
|
push_array(atrule.declarations, at_rule_declarations); |
|
} |
|
current_atrule = atrule; |
|
} |
|
if (node.type === 'Rule') { |
|
const rule = new Rule(node, this, current_atrule); |
|
if (current_atrule) { |
|
current_atrule.children.push(rule); |
|
} else if (depth <= 1) { |
|
this.children.push(rule); |
|
} |
|
} |
|
depth += 1; |
|
}, |
|
leave: ( node) => { |
|
if (node.type === 'Atrule') { |
|
stack.pop(); |
|
current_atrule = stack[stack.length - 1]; |
|
} |
|
depth -= 1; |
|
} |
|
}); |
|
} else { |
|
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]; |
|
child.apply(node); |
|
} |
|
} |
|
reify() { |
|
this.nodes_with_css_class.forEach((node) => { |
|
node.add_css_class(); |
|
}); |
|
} |
|
|
|
|
|
render(file) { |
|
if (!this.has_styles) { |
|
return { code: null, map: null }; |
|
} |
|
const code = new MagicString(this.source); |
|
walk( (this.ast.css), { |
|
enter: ( node) => { |
|
code.addSourcemapLocation(node.start); |
|
code.addSourcemapLocation(node.end); |
|
} |
|
}); |
|
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) => { |
|
if (child.is_used(this.dev)) { |
|
code.remove(c, child.node.start); |
|
child.minify(code, this.dev); |
|
c = child.node.end; |
|
} |
|
}); |
|
code.remove(c, this.source.length); |
|
return { |
|
code: code.toString(), |
|
map: code.generateMap({ |
|
includeContent: true, |
|
source: this.filename, |
|
file |
|
}) |
|
}; |
|
} |
|
|
|
|
|
validate(component) { |
|
this.children.forEach((child) => { |
|
child.validate(component); |
|
}); |
|
} |
|
|
|
|
|
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); |
|
this.children.forEach((child) => { |
|
child.warn_on_unused_selector((selector) => { |
|
component.warn( |
|
selector.node, |
|
compiler_warnings.css_unused_selector( |
|
this.source.slice(selector.node.start, selector.node.end) |
|
) |
|
); |
|
}); |
|
}); |
|
component.pop_ignores(); |
|
} |
|
} |
|
|