mirror of https://github.com/sveltejs/svelte
commit
f0fddaab82
@ -0,0 +1,233 @@
|
|||||||
|
import MagicString from 'magic-string';
|
||||||
|
import { walk } from 'estree-walker';
|
||||||
|
import { getLocator } from 'locate-character';
|
||||||
|
import Selector from './Selector';
|
||||||
|
import getCodeFrame from '../utils/getCodeFrame';
|
||||||
|
import { Validator } from '../validate/index';
|
||||||
|
import { Node, Parsed, Warning } from '../interfaces';
|
||||||
|
|
||||||
|
class Rule {
|
||||||
|
selectors: Selector[];
|
||||||
|
declarations: Node[];
|
||||||
|
|
||||||
|
constructor(node: Node) {
|
||||||
|
this.selectors = node.selector.children.map((node: Node) => new Selector(node));
|
||||||
|
this.declarations = node.block.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(node: Node, stack: Node[]) {
|
||||||
|
this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here?
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
|
||||||
|
const attr = `[${id}]`;
|
||||||
|
|
||||||
|
if (cascade) {
|
||||||
|
this.selectors.forEach(selector => {
|
||||||
|
// TODO disable cascading (without :global(...)) in v2
|
||||||
|
const { start, end, children } = selector.node;
|
||||||
|
|
||||||
|
const css = code.original;
|
||||||
|
const selectorString = css.slice(start, end);
|
||||||
|
|
||||||
|
const firstToken = children[0];
|
||||||
|
|
||||||
|
let transformed;
|
||||||
|
|
||||||
|
if (firstToken.type === 'TypeSelector') {
|
||||||
|
const insert = firstToken.end;
|
||||||
|
const head = firstToken.name === '*' ? '' : css.slice(start, insert);
|
||||||
|
const tail = css.slice(insert, end);
|
||||||
|
|
||||||
|
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
|
||||||
|
} else {
|
||||||
|
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.overwrite(start, end, transformed);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.selectors.forEach(selector => selector.transform(code, attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.declarations.forEach((declaration: Node) => {
|
||||||
|
const property = declaration.property.toLowerCase();
|
||||||
|
if (property === 'animation' || property === 'animation-name') {
|
||||||
|
declaration.value.children.forEach((block: Node) => {
|
||||||
|
if (block.type === 'Identifier') {
|
||||||
|
const name = block.name;
|
||||||
|
if (keyframes.has(name)) {
|
||||||
|
code.overwrite(block.start, block.end, keyframes.get(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Atrule {
|
||||||
|
node: Node;
|
||||||
|
|
||||||
|
constructor(node: Node) {
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
|
||||||
|
if (this.node.name !== 'keyframes') return;
|
||||||
|
|
||||||
|
this.node.expression.children.forEach((expression: Node) => {
|
||||||
|
if (expression.type === 'Identifier') {
|
||||||
|
if (expression.name.startsWith('-global-')) {
|
||||||
|
code.remove(expression.start, expression.start + 8);
|
||||||
|
} else {
|
||||||
|
const newName = `${id}-${expression.name}`;
|
||||||
|
code.overwrite(expression.start, expression.end, newName);
|
||||||
|
keyframes.set(expression.name, newName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = {};
|
||||||
|
|
||||||
|
export default class Stylesheet {
|
||||||
|
source: string;
|
||||||
|
parsed: Parsed;
|
||||||
|
cascade: boolean;
|
||||||
|
filename: string;
|
||||||
|
|
||||||
|
hasStyles: boolean;
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
nodes: (Rule|Atrule)[];
|
||||||
|
rules: Rule[];
|
||||||
|
atrules: Atrule[];
|
||||||
|
|
||||||
|
constructor(source: string, parsed: Parsed, filename: string, cascade: boolean) {
|
||||||
|
this.source = source;
|
||||||
|
this.parsed = parsed;
|
||||||
|
this.cascade = cascade;
|
||||||
|
this.filename = filename;
|
||||||
|
|
||||||
|
this.id = `svelte-${parsed.hash}`;
|
||||||
|
|
||||||
|
this.nodes = [];
|
||||||
|
this.rules = [];
|
||||||
|
this.atrules = [];
|
||||||
|
|
||||||
|
if (parsed.css && parsed.css.children.length) {
|
||||||
|
this.hasStyles = true;
|
||||||
|
|
||||||
|
const stack: Atrule[] = [];
|
||||||
|
let currentAtrule: Atrule = null;
|
||||||
|
|
||||||
|
walk(this.parsed.css, {
|
||||||
|
enter: (node: Node) => {
|
||||||
|
if (node.type === 'Atrule') {
|
||||||
|
const atrule = currentAtrule = new Atrule(node);
|
||||||
|
stack.push(atrule);
|
||||||
|
|
||||||
|
this.nodes.push(atrule);
|
||||||
|
this.atrules.push(atrule);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'Rule' && (!currentAtrule || /(media|supports|document)/.test(currentAtrule.node.name))) {
|
||||||
|
const rule = new Rule(node);
|
||||||
|
this.nodes.push(rule);
|
||||||
|
this.rules.push(rule);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
leave: (node: Node) => {
|
||||||
|
if (node.type === 'Atrule') {
|
||||||
|
stack.pop();
|
||||||
|
currentAtrule = stack[stack.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.hasStyles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(node: Node, stack: Node[]) {
|
||||||
|
if (!this.hasStyles) return;
|
||||||
|
|
||||||
|
if (this.cascade) {
|
||||||
|
if (stack.length === 0) node._needsCssAttribute = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.rules.length; i += 1) {
|
||||||
|
const rule = this.rules[i];
|
||||||
|
rule.apply(node, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(cssOutputFilename: string) {
|
||||||
|
if (!this.hasStyles) {
|
||||||
|
return { css: null, cssMap: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = new MagicString(this.source);
|
||||||
|
code.remove(0, this.parsed.css.start + 7);
|
||||||
|
code.remove(this.parsed.css.end - 8, this.source.length);
|
||||||
|
|
||||||
|
const keyframes = new Map();
|
||||||
|
this.atrules.forEach((atrule: Atrule) => {
|
||||||
|
atrule.transform(code, this.id, keyframes);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rules.forEach((rule: Rule) => {
|
||||||
|
rule.transform(code, this.id, keyframes, this.cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
css: code.toString(),
|
||||||
|
cssMap: code.generateMap({
|
||||||
|
includeContent: true,
|
||||||
|
source: this.filename,
|
||||||
|
file: cssOutputFilename
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(validator: Validator) {
|
||||||
|
this.rules.forEach(rule => {
|
||||||
|
rule.selectors.forEach(selector => {
|
||||||
|
selector.validate(validator);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
warnOnUnusedSelectors(onwarn: (warning: Warning) => void) {
|
||||||
|
if (this.cascade) return;
|
||||||
|
|
||||||
|
let locator;
|
||||||
|
|
||||||
|
this.rules.forEach((rule: Rule) => {
|
||||||
|
rule.selectors.forEach(selector => {
|
||||||
|
if (!selector.used) {
|
||||||
|
const pos = selector.node.start;
|
||||||
|
|
||||||
|
if (!locator) locator = getLocator(this.source);
|
||||||
|
const { line, column } = locator(pos);
|
||||||
|
|
||||||
|
const frame = getCodeFrame(this.source, line, column);
|
||||||
|
const message = `Unused CSS selector`;
|
||||||
|
|
||||||
|
onwarn({
|
||||||
|
message,
|
||||||
|
frame,
|
||||||
|
loc: { line: line + 1, column },
|
||||||
|
pos,
|
||||||
|
filename: this.filename,
|
||||||
|
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,104 +0,0 @@
|
|||||||
import MagicString from 'magic-string';
|
|
||||||
import { groupSelectors, isGlobalSelector, walkRules } from '../../utils/css';
|
|
||||||
import Generator from '../Generator';
|
|
||||||
import { Node } from '../../interfaces';
|
|
||||||
|
|
||||||
const commentsPattern = /\/\*[\s\S]*?\*\//g;
|
|
||||||
|
|
||||||
export default function processCss(
|
|
||||||
generator: Generator,
|
|
||||||
code: MagicString,
|
|
||||||
cascade: boolean
|
|
||||||
) {
|
|
||||||
const css = generator.parsed.css.content.styles;
|
|
||||||
const offset = generator.parsed.css.content.start;
|
|
||||||
|
|
||||||
const attr = `[svelte-${generator.parsed.hash}]`;
|
|
||||||
|
|
||||||
const keyframes = new Map();
|
|
||||||
|
|
||||||
function walkKeyframes(node: Node) {
|
|
||||||
if (node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes') {
|
|
||||||
node.expression.children.forEach((expression: Node) => {
|
|
||||||
if (expression.type === 'Identifier') {
|
|
||||||
if (expression.name.startsWith('-global-')) {
|
|
||||||
code.remove(expression.start, expression.start + 8);
|
|
||||||
} else {
|
|
||||||
const newName = `svelte-${generator.parsed.hash}-${expression.name}`;
|
|
||||||
code.overwrite(expression.start, expression.end, newName);
|
|
||||||
keyframes.set(expression.name, newName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (node.children) {
|
|
||||||
node.children.forEach(walkKeyframes);
|
|
||||||
} else if (node.block) {
|
|
||||||
walkKeyframes(node.block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generator.parsed.css.children.forEach(walkKeyframes);
|
|
||||||
|
|
||||||
function transform(rule: Node) {
|
|
||||||
rule.selector.children.forEach((selector: Node) => {
|
|
||||||
if (cascade) {
|
|
||||||
// TODO disable cascading (without :global(...)) in v2
|
|
||||||
const start = selector.start - offset;
|
|
||||||
const end = selector.end - offset;
|
|
||||||
|
|
||||||
const selectorString = css.slice(start, end);
|
|
||||||
|
|
||||||
const firstToken = selector.children[0];
|
|
||||||
|
|
||||||
let transformed;
|
|
||||||
|
|
||||||
if (firstToken.type === 'TypeSelector') {
|
|
||||||
const insert = firstToken.end - offset;
|
|
||||||
const head = firstToken.name === '*' ? css.slice(firstToken.end - offset, insert) : css.slice(start, insert);
|
|
||||||
const tail = css.slice(insert, end);
|
|
||||||
|
|
||||||
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
|
|
||||||
} else {
|
|
||||||
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
code.overwrite(selector.start, selector.end, transformed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rule.block.children.forEach((block: Node) => {
|
|
||||||
if (block.type === 'Declaration') {
|
|
||||||
const property = block.property.toLowerCase();
|
|
||||||
if (property === 'animation' || property === 'animation-name') {
|
|
||||||
block.value.children.forEach((block: Node) => {
|
|
||||||
if (block.type === 'Identifier') {
|
|
||||||
const name = block.name;
|
|
||||||
if (keyframes.has(name)) {
|
|
||||||
code.overwrite(block.start, block.end, keyframes.get(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
walkRules(generator.parsed.css.children, transform);
|
|
||||||
|
|
||||||
if (!cascade) {
|
|
||||||
generator.selectors.forEach(selector => {
|
|
||||||
selector.transform(code, attr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove comments. TODO would be nice if this was exposed in css-tree
|
|
||||||
let match;
|
|
||||||
while ((match = commentsPattern.exec(css))) {
|
|
||||||
const start = match.index + offset;
|
|
||||||
const end = start + match[0].length;
|
|
||||||
|
|
||||||
code.remove(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
return code.slice(generator.parsed.css.content.start, generator.parsed.css.content.end);
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import { Node } from '../interfaces';
|
|
||||||
|
|
||||||
export function isGlobalSelector(block: Node[]) {
|
|
||||||
return block[0].type === 'PseudoClassSelector' && block[0].name === 'global';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function groupSelectors(selector: Node) {
|
|
||||||
let block = {
|
|
||||||
global: selector.children[0].type === 'PseudoClassSelector' && selector.children[0].name === 'global',
|
|
||||||
selectors: [],
|
|
||||||
combinator: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const blocks = [block];
|
|
||||||
|
|
||||||
selector.children.forEach((child: Node, i: number) => {
|
|
||||||
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
|
|
||||||
const next = selector.children[i + 1];
|
|
||||||
|
|
||||||
block = {
|
|
||||||
global: next.type === 'PseudoClassSelector' && next.name === 'global',
|
|
||||||
selectors: [],
|
|
||||||
combinator: child
|
|
||||||
};
|
|
||||||
|
|
||||||
blocks.push(block);
|
|
||||||
} else {
|
|
||||||
block.selectors.push(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function walkRules(nodes: Node[], callback: (node: Node) => void) {
|
|
||||||
nodes.forEach((node: Node) => {
|
|
||||||
if (node.type === 'Rule') {
|
|
||||||
callback(node);
|
|
||||||
} else if (node.type === 'Atrule') {
|
|
||||||
if (node.name === 'media' || node.name === 'supports' || node.name === 'document') {
|
|
||||||
walkRules(node.block.children, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { groupSelectors, isGlobalSelector, walkRules } from '../../utils/css';
|
|
||||||
import { Validator } from '../index';
|
|
||||||
import { Node } from '../../interfaces';
|
|
||||||
|
|
||||||
export default function validateCss(validator: Validator, css: Node) {
|
|
||||||
walkRules(css.children, rule => {
|
|
||||||
rule.selector.children.forEach(validateSelector);
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateSelector(selector: Node) {
|
|
||||||
const blocks = groupSelectors(selector);
|
|
||||||
|
|
||||||
blocks.forEach((block) => {
|
|
||||||
let i = block.selectors.length;
|
|
||||||
while (i-- > 1) {
|
|
||||||
const part = block.selectors[i];
|
|
||||||
if (part.type === 'PseudoClassSelector' && part.name === 'global') {
|
|
||||||
validator.error(`:global(...) must be the first element in a compound selector`, part.start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let start = 0;
|
|
||||||
let end = blocks.length;
|
|
||||||
|
|
||||||
for (; start < end; start += 1) {
|
|
||||||
if (!blocks[start].global) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; end > start; end -= 1) {
|
|
||||||
if (!blocks[end - 1].global) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = start; i < end; i += 1) {
|
|
||||||
if (blocks[i].global) {
|
|
||||||
validator.error(`:global(...) can be at the start or end of a selector sequence, but not in the middle`, blocks[i].selectors[0].start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,7 @@
|
|||||||
|
<p class='foo'>red</p>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.foo {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
[svelte-2772200924].foo, [svelte-2772200924] .foo {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=output.css.map */
|
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"file": "output.css",
|
||||||
|
"sources": [
|
||||||
|
"input.html"
|
||||||
|
],
|
||||||
|
"sourcesContent": [
|
||||||
|
"<p class='foo'>red</p>\n\n<style>\n\t.foo {\n\t\tcolor: red;\n\t}\n</style>"
|
||||||
|
],
|
||||||
|
"names": [],
|
||||||
|
"mappings": "AAEO;CACN,iDAAI;;;AAGL"
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
export function test ({ assert, smcCss, locateInSource, locateInGeneratedCss }) {
|
||||||
|
const expected = locateInSource( '.foo' );
|
||||||
|
|
||||||
|
const loc = locateInGeneratedCss( '.foo' );
|
||||||
|
|
||||||
|
const actual = smcCss.originalPositionFor({
|
||||||
|
line: loc.line + 1,
|
||||||
|
column: loc.column
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual( actual, {
|
||||||
|
source: 'input.html',
|
||||||
|
name: null,
|
||||||
|
line: expected.line + 1,
|
||||||
|
column: expected.column
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue