Skip to content

Commit

Permalink
Merge pull request #680 from sveltejs/gh-679
Browse files Browse the repository at this point in the history
Only apply svelte-123xyz attributes where necessary
  • Loading branch information
Rich-Harris committed Jul 5, 2017
2 parents ae97cfc + 7a752df commit ef33466
Show file tree
Hide file tree
Showing 130 changed files with 1,179 additions and 133 deletions.
65 changes: 63 additions & 2 deletions src/generators/Generator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import MagicString, { Bundle } from 'magic-string';
import { walk } from 'estree-walker';
import { getLocator } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import isReference from '../utils/isReference';
import flattenReference from '../utils/flattenReference';
import globalWhitelist from '../utils/globalWhitelist';
Expand All @@ -13,6 +15,8 @@ import annotateWithScopes from '../utils/annotateWithScopes';
import clone from '../utils/clone';
import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block';
import { walkRules } from '../utils/css';
import Selector from './Selector';
import { Node, Parsed, CompileOptions } from '../interfaces';

const test = typeof global !== 'undefined' && global.__svelte_test;
Expand Down Expand Up @@ -40,6 +44,8 @@ export default class Generator {
cssId: string;
usesRefs: boolean;

selectors: Selector[];

importedNames: Set<string>;
aliases: Map<string, string>;
usedNames: Set<string>;
Expand Down Expand Up @@ -71,10 +77,24 @@ export default class Generator {
this.expectedProperties = new Set();

this.code = new MagicString(source);
this.usesRefs = false;

// styles
this.cascade = options.cascade !== false; // TODO remove this option in v2
this.css = parsed.css ? processCss(parsed, this.code, this.cascade) : null;
this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
this.usesRefs = false;
this.selectors = [];

if (parsed.css) {
walkRules(parsed.css.children, node => {
node.selector.children.forEach((child: Node) => {
this.selectors.push(new Selector(child));
});
});

this.css = processCss(this, this.code, this.cascade);
} else {
this.css = null;
}

// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
Expand Down Expand Up @@ -211,6 +231,20 @@ export default class Generator {
};
}

applyCss(node: Node, stack: Node[]) {
if (!this.cssId) return;

if (this.cascade) {
if (stack.length === 0) node._needsCssAttribute = true;
return;
}

for (let i = 0; i < this.selectors.length; i += 1) {
const selector = this.selectors[i];
selector.apply(node, stack);
}
}

findDependencies(
contextDependencies: Map<string, string[]>,
indexes: Map<string, string>,
Expand Down Expand Up @@ -590,4 +624,31 @@ export default class Generator {
this.namespace = namespace;
this.templateProperties = templateProperties;
}

warnOnUnusedSelectors() {
if (this.cascade) return;

let locator;

this.selectors.forEach((selector: 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`;

this.options.onwarn({
message,
frame,
loc: { line: line + 1, column },
pos,
filename: this.options.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
});
}
});
}
}
180 changes: 180 additions & 0 deletions src/generators/Selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import MagicString from 'magic-string';
import { groupSelectors, isGlobalSelector, walkRules } from '../utils/css';
import { Node } from '../interfaces';

export default class Selector {
node: Node;
blocks: any; // TODO
parts: Node[];
used: boolean;

constructor(node: Node) {
this.node = node;

this.blocks = groupSelectors(this.node);

// take trailing :global(...) selectors out of consideration
let i = node.children.length;
while (i > 2) {
const last = node.children[i-1];
const penultimate = node.children[i-2];

if (last.type === 'PseudoClassSelector' && last.name === 'global') {
i -= 2;
} else {
break;
}
}

this.parts = node.children.slice(0, i);

this.used = this.blocks[0].global;
}

apply(node: Node, stack: Node[]) {
const applies = selectorAppliesTo(this.parts, node, stack.slice());

if (applies) {
this.used = true;

// add svelte-123xyz attribute to outermost and innermost
// elements — no need to add it to intermediate elements
node._needsCssAttribute = true;
if (stack[0] && this.node.children.find(isDescendantSelector)) stack[0]._needsCssAttribute = true;
}
}

transform(code: MagicString, attr: string) {
function encapsulateBlock(block) {
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') continue;

if (selector.type === 'TypeSelector' && selector.name === '*') {
code.overwrite(selector.start, selector.end, attr);
} else {
code.appendLeft(selector.end, attr);
}

return;
}
}

this.blocks.forEach((block, i) => {
if (block.global) {
const selector = block.selectors[0];
const first = selector.children[0];
const last = selector.children[selector.children.length - 1];
code.remove(selector.start, first.start).remove(last.end, selector.end);
} else if (i === 0 || i === this.blocks.length - 1) {
encapsulateBlock(block);
}
});
}
}

function isDescendantSelector(selector: Node) {
return selector.type === 'WhiteSpace' || selector.type === 'Combinator';
}

function selectorAppliesTo(parts: Node[], node: Node, stack: Node[]): boolean {
let i = parts.length;
let j = stack.length;

while (i--) {
if (!node) {
return parts.every((part: Node) => {
return part.type === 'Combinator' || (part.type === 'PseudoClassSelector' && part.name === 'global');
});
}

const part = parts[i];

if (part.type === 'PseudoClassSelector' && part.name === 'global') {
// TODO shouldn't see this here... maybe we should enforce that :global(...)
// cannot be sandwiched between non-global selectors?
return false;
}

if (part.type === 'PseudoClassSelector' || part.type === 'PseudoElementSelector') {
continue;
}

if (part.type === 'ClassSelector') {
if (!attributeMatches(node, 'class', part.name, '~=', false)) return false;
}

else if (part.type === 'IdSelector') {
if (!attributeMatches(node, 'id', part.name, '=', false)) return false;
}

else if (part.type === 'AttributeSelector') {
if (!attributeMatches(node, part.name.name, part.value && unquote(part.value.value), part.operator, part.flags)) return false;
}

else if (part.type === 'TypeSelector') {
if (part.name === '*') return true;
if (node.name !== part.name) return false;
}

else if (part.type === 'WhiteSpace') {
parts = parts.slice(0, i);

while (stack.length) {
if (selectorAppliesTo(parts, stack.pop(), stack)) {
return true;
}
}

return false;
}

else if (part.type === 'Combinator') {
if (part.name === '>') {
return selectorAppliesTo(parts.slice(0, i), stack.pop(), stack);
}

// TODO other combinators
return true;
}

else {
// bail. TODO figure out what these could be
return true;
}
}

return true;
}

const operators = {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
'*=': (value: string, flags: string) => new RegExp(value, flags)
};

function attributeMatches(node: Node, name: string, expectedValue: string, operator: string, caseInsensitive: boolean) {
const attr = node.attributes.find((attr: Node) => attr.name === name);
if (!attr) return false;
if (attr.value === true) return operator === null;
if (isDynamic(attr.value)) return true;

const actualValue = attr.value[0].data;

const pattern = operators[operator](expectedValue, caseInsensitive ? 'i' : '');
return pattern.test(actualValue);
}

function isDynamic(value: Node) {
return value.length > 1 || value[0].type !== 'Text';
}

function unquote(str: string) {
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
return str.slice(1, str.length - 1);
}
}
4 changes: 3 additions & 1 deletion src/generators/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ export default function dom(

const { block, state } = preprocess(generator, namespace, parsed.html);

generator.warnOnUnusedSelectors();

parsed.html.children.forEach((node: Node) => {
visit(generator, block, state, node);
visit(generator, block, state, node, []);
});

const builder = new CodeBuilder();
Expand Down
Loading

0 comments on commit ef33466

Please sign in to comment.