blob: 001fd435a74068151964bfe9bcb4d1c8b8e930ce [file] [edit]
// 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 '../parser.dart';
/// CSS polyfill emits CSS to be understood by older parsers that which do not
/// understand (var, calc, etc.).
class PolyFill {
final Messages _messages;
Map<String, VarDefinition> _allVarDefinitions = <String, VarDefinition>{};
Set<StyleSheet> allStyleSheets = <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);
/// 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}) {
if (includes != null) {
processVarDefinitions(includes);
}
processVars(styleSheet);
// Remove all var definitions for this style sheet.
_RemoveVarDefinitions().visitTree(styleSheet);
}
/// Process all includes looking for var definitions.
void processVarDefinitions(List<StyleSheet> includes) {
for (var include in includes) {
_allVarDefinitions = (_VarDefinitionsIncludes(_allVarDefinitions)
..visitTree(include))
.varDefs;
}
}
void processVars(StyleSheet styleSheet) {
// Build list of all var definitions.
var mainStyleSheetVarDefs = (_VarDefAndUsage(_messages, _allVarDefinitions)
..visitTree(styleSheet))
.varDefs;
// Resolve all definitions to a non-VarUsage (terminal expression).
mainStyleSheetVarDefs.forEach((key, value) {
if (value.expression is Expressions) {
for (var _ in (value.expression as Expressions).expressions.toList()) {
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);
@override
void visitTree(StyleSheet tree) {
visitStyleSheet(tree);
}
@override
void visitVarDefinition(VarDefinition node) {
// Replace with latest variable definition.
varDefs[node.definedName] = node;
super.visitVarDefinition(node);
}
@override
void visitVarDefinitionDirective(VarDefinitionDirective node) {
visitVarDefinition(node.def);
}
}
/// Find var- definitions in a style sheet.
/// [found] list of known definitions.
class _VarDefAndUsage extends Visitor {
static const int _maxExpansions = 256;
final Messages _messages;
final Map<String, VarDefinition> _knownVarDefs;
final varDefs = <String, VarDefinition>{};
VarDefinition? currVarDefinition;
List<Expression>? currentExpressions;
int _expansions = 0;
_VarDefAndUsage(this._messages, this._knownVarDefs);
@override
void visitTree(StyleSheet tree) {
visitStyleSheet(tree);
}
@override
void visitVarDefinition(VarDefinition node) {
// Replace with latest variable definition.
currVarDefinition = node;
_knownVarDefs[node.definedName] = node;
varDefs[node.definedName] = node;
super.visitVarDefinition(node);
currVarDefinition = null;
}
@override
void visitVarDefinitionDirective(VarDefinitionDirective node) {
visitVarDefinition(node.def);
}
@override
void visitExpressions(Expressions node) {
var oldExpressions = currentExpressions;
var expressions = node.expressions;
currentExpressions = expressions;
var index = 0;
while (index < expressions.length) {
var current = expressions[index];
current.visit(this);
if (index < expressions.length &&
identical(expressions[index], current)) {
index++;
} else if (_expansions++ >= _maxExpansions) {
break;
}
}
currentExpressions = oldExpressions;
}
@override
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;
if (expressions == null) return;
var index = expressions.indexOf(node);
if (index < 0) return;
var def = _knownVarDefs[node.name];
if (def == currVarDefinition) {
def = null;
}
if (def != null && !def.badUsage) {
_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) {
if (defaultValue is VarUsage) {
terminalDefaults.addAll(resolveUsageTerminal(defaultValue));
} else {
terminalDefaults.add(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);
if (def == null) {
_messages.warning('Variable is not defined.', node.span);
}
}
var oldExpressions = currentExpressions;
currentExpressions = node.defaultValues;
super.visitVarUsage(node);
currentExpressions = oldExpressions;
}
List<Expression> resolveUsageTerminal(VarUsage usage,
[Set<VarDefinition>? visited]) {
var result = <Expression>[];
visited ??= <VarDefinition>{};
var varDef = _knownVarDefs[usage.name];
if (varDef != null && (varDef.badUsage || !visited.add(varDef))) {
varDef = null;
}
List<Expression> expressions;
if (varDef == null) {
// VarDefinition not found try the defaultValues.
expressions = usage.defaultValues;
} else if (varDef.expression is Expressions) {
// Use the VarDefinition found.
expressions = (varDef.expression as Expressions).expressions;
} else {
expressions = const <Expression>[];
}
for (var expr in expressions) {
if (expr is VarUsage) {
// Get terminal value.
result.addAll(resolveUsageTerminal(expr, visited));
} else {
result.add(expr);
}
}
return result;
}
void _resolveVarUsage(
List<Expression> expressions, int index, VarDefinition def) {
if (def.expression is! Expressions) return;
var defExpressions = (def.expression as Expressions).expressions;
expressions.replaceRange(index, index + 1, defExpressions);
}
}
/// Remove all var definitions.
class _RemoveVarDefinitions extends Visitor {
@override
void visitTree(StyleSheet tree) {
visitStyleSheet(tree);
}
@override
void visitStyleSheet(StyleSheet ss) {
ss.topLevels.removeWhere((e) => e is VarDefinitionDirective);
super.visitStyleSheet(ss);
}
@override
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,
[Set<VarDefinition>? visited]) {
visited ??= <VarDefinition>{};
if (!visited.add(varDef)) return varDef;
if (varDef.expression is! Expressions) return varDef;
var expressions = varDef.expression as Expressions;
for (var expr in expressions.expressions) {
if (expr is VarUsage) {
var usageName = expr.name;
var foundDef = varDefs[usageName];
// If foundDef is unknown check if defaultValues; if it exist then resolve
// to terminal value.
if (foundDef == null || visited.contains(foundDef)) {
// 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.defaultValues;
var replaceExprs = expressions.expressions;
var idx = replaceExprs.indexOf(expr);
if (idx != -1) {
replaceExprs.replaceRange(idx, idx + 1, defaultValues);
}
return varDef;
}
return _findTerminalVarDefinition(varDefs, foundDef, visited);
} else {
// Return real CSS property.
return varDef;
}
}
// Didn't point to a var definition that existed.
return varDef;
}