package xapi.dev.ui;
import com.github.javaparser.ast.expr.*;
import xapi.dev.source.ClassBuffer;
import xapi.dev.source.PrintBuffer;
import xapi.fu.Lazy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Created by James X. Nelson (james @wetheinter.net) on 6/19/16.
*/
public class CssFeatureGenerator extends UiFeatureGenerator {
@Override
public UiVisitScope startVisit(
UiGeneratorTools service, UiComponentGenerator generator, ContainerMetadata container, UiAttrExpr attr
) {
final Expression value = attr.getExpression();
boolean isClassAttr = "class".equalsIgnoreCase(attr.getNameString());
boolean isStyleAttr = "style".equalsIgnoreCase(attr.getNameString());
boolean isCssAttr = "css".equalsIgnoreCase(attr.getNameString());
if (! (isClassAttr || isStyleAttr || isCssAttr ) ) {
throw new IllegalArgumentException("Unsupported css feature name: " + attr.getNameString()+"; " +
"Expected names are: class, style or css. See CssFeatureGenerator for details.");
}
List<CssContainerExpr> containers = new ArrayList<>();
if (value instanceof CssBlockExpr) {
containers.addAll(((CssBlockExpr)value).getContainers());
} else if (value instanceof CssContainerExpr) {
containers.add((CssContainerExpr) value);
} else if (value instanceof StringLiteralExpr) {
container.getStyle().addClassNames(((StringLiteralExpr)value).getValue().trim().split("\\s+"));
} else {
throw new IllegalArgumentException("Cannot assign a node of type " + value.getClass() + " to a css feature; bad data: " + value);
}
boolean hasDynamicCss = checkForDynamism(containers);
container.getStyle().setDynamicRules(hasDynamicCss);
Set<CssSelectorExpr> selectors = extractSelectors(containers);
if (isStyleAttr) {
// style features do not allow selectors;
// they are meant to be applied directly to elements.
if (!selectors.isEmpty()) {
throw new IllegalArgumentException("Cannot use css blocks that have selectors in a style feature;" +
"\nExpected format is: `style=.{ color: blue; }`, not: " + containers);
}
// Now transform the css ast into commands that will apply style to an element.
container.getStyle().addApplied(containers);
return UiVisitScope.DEFAULT_CONTAINER;
}
if (isClassAttr) {
// if class features have selectors, they must begin with a .class-name.
Set<String> classes = new LinkedHashSet<>(container.getStyle().getClassNames());
if (selectors.isEmpty()) {
// no selector? We'll generate a classname for you...
if (!containers.isEmpty()) {
String newCls = container.getNameGen().newClass();
List<String> parts = Collections.singletonList("." + newCls);
selectors.forEach(selector->selector.setParts(parts));
classes.add(newCls);
}
} else {
// If you specified selectors, they must all start with a .className part,
// and all such classnames will be added to element (without any subparts).
selectors.forEach(selector->{
if (selector.getParts().isEmpty()) {
String newCls = container.getNameGen().newClass();
selector.setParts(Collections.singletonList("." + newCls));
classes.add(newCls);
} else {
String part0 = selector.getParts().get(0);
if (!part0.startsWith(".")) {
throw new IllegalArgumentException("You cannot have a class= feature that contains css selectors " +
"which do not start with a .className.\nOffending selector: " + selector +
"\nOffending ui: " + container.getUi());
}
part0 = sliceClassName(part0);
classes.add(part0);
}
});
}
// Alright, we have a stack of classnames that we want to set as this element's class attribute,
// as well as a set of rules to add to a stylesheet (which we must import).
final ClassBuffer cb = container.getSourceBuilder().getClassBuffer();
String varName = container.newVarName(container.getRefName()+"Classes");
String lazy = cb.addImport(Lazy.class);
final PrintBuffer initializer = cb.createField(
lazy + "<String[]>",
varName
).getInitializer()
.print(lazy)
.println(".deferred1(()->new String[]{")
.indent();
String prefix = "";
for (String cls : classes) {
initializer
.print(prefix)
.print("\"" + cls + "\"");
prefix = ", ";
}
initializer.println().outdent().println("})");
// For now, we will just export one method with the classnames, and put the rules into the StyleMetadata
// since end user implementations will likely want to transform the rules individually.
container.getStyle().addClassNames(classes);
container.getStyle().addRules(containers);
} else {
// A css = .{ } block of css rules; we'll just add them to the style metadata...
container.getStyle().addRules(containers);
}
return UiVisitScope.DEFAULT_CONTAINER;
}
private boolean checkForDynamism(List<CssContainerExpr> containers) {
for (CssContainerExpr container : containers) {
for (CssRuleExpr rule : container.getRules()) {
if (isDynamic(rule.getKey())) {
return true;
}
if (isDynamic(rule.getValue())) {
return true;
}
}
}
return false;
}
private boolean isDynamic(Expression key) {
if (key instanceof LiteralExpr) {
// TODO narrow literals that might contain dynamism (like json nodes, for example)
return false;
}
if (key instanceof NameExpr) {
return ((NameExpr)key).getName().startsWith("$");
}
return true;
}
private String sliceClassName(String name) {
int i = 0;
if (name.startsWith(".")) {
name = name.substring(1);
}
while (i < name.length()) {
char c = name.charAt(i);
switch (c) {
case '-':
case '_':
assert i > 0 : "Not a valid classname " + name;
i++;
break;
default:
if (Character.isLetterOrDigit(c)) {
assert i > 0 || Character.isLetter(c) : "Not a valid classname " + name;
i++;
} else {
assert i > 0 : "Not a valid classname " + name;
// TODO validate that this is a valid css selector join character, like . > ~ + [ (
assert c == '.' || c == '>' || c == '~' || c == '+' || c == '[' || c == '(' :
"Not a valid selector: " + name;
return name.substring(0, i);
}
}
}
return name;
}
private Set<CssSelectorExpr> extractSelectors(List<CssContainerExpr> containers) {
Set<CssSelectorExpr> selectors = new LinkedHashSet<>();
containers.stream()
.map(CssContainerExpr::getSelectors)
.flatMap(List::stream)
.forEach(selectors::add);
return selectors;
}
}