blob: bdd833031c1557024104b407232cca9e064f9f14 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of csslib.parser;
/**
* CSS polyfill emits CSS to be understood by older parsers that which do not
* understand (var, calc, etc.).
*/
class PolyFill {
final Messages _messages;
final bool _warningsAsErrors;
Map<String, VarDefinition> _allVarDefinitions =
new Map<String, VarDefinition>();
Set<StyleSheet> allStyleSheets = new Set<StyleSheet>();
/**
* [_pseudoElements] list of known pseudo attributes found in HTML, any
* CSS pseudo-elements 'name::custom-element' is mapped to the manged name
* associated with the pseudo-element key.
*/
PolyFill(this._messages, this._warningsAsErrors);
/**
* Run the analyzer on every file that is a style sheet or any component that
* has a style tag.
*/
void process(StyleSheet styleSheet, {List<StyleSheet> includes: null}) {
if (includes != null) {
processVarDefinitions(includes);
}
processVars(styleSheet);
// Remove all var definitions for this style sheet.
new _RemoveVarDefinitions().visitTree(styleSheet);
}
/** Process all includes looking for var definitions. */
void processVarDefinitions(List<StyleSheet> includes) {
for (var include in includes) {
_allVarDefinitions = (new _VarDefinitionsIncludes(_allVarDefinitions)
..visitTree(include)).varDefs;
}
}
void processVars(StyleSheet styleSheet) {
// Build list of all var definitions.
var mainStyleSheetVarDefs = (new _VarDefAndUsage(
this._messages, _allVarDefinitions)..visitTree(styleSheet)).varDefs;
// Resolve all definitions to a non-VarUsage (terminal expression).
mainStyleSheetVarDefs.forEach((key, value) {
for (Expression expr in (value.expression as Expressions).expressions) {
mainStyleSheetVarDefs[key] =
_findTerminalVarDefinition(_allVarDefinitions, value);
}
});
}
}
/** Build list of all var definitions in all includes. */
class _VarDefinitionsIncludes extends Visitor {
final Map<String, VarDefinition> varDefs;
_VarDefinitionsIncludes(this.varDefs);
void visitTree(StyleSheet tree) {
visitStyleSheet(tree);
}
visitVarDefinition(VarDefinition node) {
// Replace with latest variable definition.
varDefs[node.definedName] = node;
super.visitVarDefinition(node);
}
void visitVarDefinitionDirective(VarDefinitionDirective node) {
visitVarDefinition(node.def);
}
}
/**
* Find var- definitions in a style sheet.
* [found] list of known definitions.
*/
class _VarDefAndUsage extends Visitor {
final Messages _messages;
final Map<String, VarDefinition> _knownVarDefs;
final Map<String, VarDefinition> varDefs = new Map<String, VarDefinition>();
VarDefinition currVarDefinition;
List<Expression> currentExpressions;
_VarDefAndUsage(this._messages, this._knownVarDefs);
void visitTree(StyleSheet tree) {
visitStyleSheet(tree);
}
visitVarDefinition(VarDefinition node) {
// Replace with latest variable definition.
currVarDefinition = node;
_knownVarDefs[node.definedName] = node;
varDefs[node.definedName] = node;
super.visitVarDefinition(node);
currVarDefinition = null;
}
void visitVarDefinitionDirective(VarDefinitionDirective node) {
visitVarDefinition(node.def);
}
void visitExpressions(Expressions node) {
currentExpressions = node.expressions;
super.visitExpressions(node);
currentExpressions = null;
}
void visitVarUsage(VarUsage node) {
if (currVarDefinition != null && currVarDefinition.badUsage) return;
// Don't process other var() inside of a varUsage. That implies that the
// default is a var() too. Also, don't process any var() inside of a
// varDefinition (they're just place holders until we've resolved all real
// usages.
var expressions = currentExpressions;
var index = expressions.indexOf(node);
assert(index >= 0);
var def = _knownVarDefs[node.name];
if (def != null) {
if (def.badUsage) {
// Remove any expressions pointing to a bad var definition.
expressions.removeAt(index);
return;
}
_resolveVarUsage(currentExpressions, index,
_findTerminalVarDefinition(_knownVarDefs, def));
} else if (node.defaultValues.any((e) => e is VarUsage)) {
// Don't have a VarDefinition need to use default values resolve all
// default values.
var terminalDefaults = <Expression>[];
for (var defaultValue in node.defaultValues) {
terminalDefaults.addAll(resolveUsageTerminal(defaultValue));
}
expressions.replaceRange(index, index + 1, terminalDefaults);
} else if (node.defaultValues.isNotEmpty) {
// No VarDefinition but default value is a terminal expression; use it.
expressions.replaceRange(index, index + 1, node.defaultValues);
} else {
if (currVarDefinition != null) {
currVarDefinition.badUsage = true;
var mainStyleSheetDef = varDefs[node.name];
if (mainStyleSheetDef != null) {
varDefs.remove(currVarDefinition.property);
}
}
// Remove var usage that points at an undefined definition.
expressions.removeAt(index);
_messages.warning("Variable is not defined.", node.span);
}
var oldExpressions = currentExpressions;
currentExpressions = node.defaultValues;
super.visitVarUsage(node);
currentExpressions = oldExpressions;
}
List<Expression> resolveUsageTerminal(VarUsage usage) {
var result = [];
var varDef = _knownVarDefs[usage.name];
var expressions;
if (varDef == null) {
// VarDefinition not found try the defaultValues.
expressions = usage.defaultValues;
} else {
// Use the VarDefinition found.
expressions = (varDef.expression as Expressions).expressions;
}
for (var expr in expressions) {
if (expr is VarUsage) {
// Get terminal value.
result.addAll(resolveUsageTerminal(expr));
}
}
// We're at a terminal just return the VarDefinition expression.
if (result.isEmpty && varDef != null) {
result = (varDef.expression as Expressions).expressions;
}
return result;
}
_resolveVarUsage(List<Expression> expressions, int index, VarDefinition def) {
var defExpressions = (def.expression as Expressions).expressions;
expressions.replaceRange(index, index + 1, defExpressions);
}
}
/** Remove all var definitions. */
class _RemoveVarDefinitions extends Visitor {
void visitTree(StyleSheet tree) {
visitStyleSheet(tree);
}
void visitStyleSheet(StyleSheet ss) {
ss.topLevels.removeWhere((e) => e is VarDefinitionDirective);
super.visitStyleSheet(ss);
}
void visitDeclarationGroup(DeclarationGroup node) {
node.declarations.removeWhere((e) => e is VarDefinition);
super.visitDeclarationGroup(node);
}
}
/** Find terminal definition (non VarUsage implies real CSS value). */
VarDefinition _findTerminalVarDefinition(
Map<String, VarDefinition> varDefs, VarDefinition varDef) {
var expressions = varDef.expression as Expressions;
for (var expr in expressions.expressions) {
if (expr is VarUsage) {
var usageName = (expr as VarUsage).name;
var foundDef = varDefs[usageName];
// If foundDef is unknown check if defaultValues; if it exist then resolve
// to terminal value.
if (foundDef == null) {
// We're either a VarUsage or terminal definition if in varDefs;
// either way replace VarUsage with it's default value because the
// VarDefinition isn't found.
var defaultValues = (expr as VarUsage).defaultValues;
var replaceExprs = expressions.expressions;
assert(replaceExprs.length == 1);
replaceExprs.replaceRange(0, 1, defaultValues);
return varDef;
}
if (foundDef is VarDefinition) {
return _findTerminalVarDefinition(varDefs, foundDef);
}
} else {
// Return real CSS property.
return varDef;
}
}
// Didn't point to a var definition that existed.
return varDef;
}