package com.intellij.flex.uiDesigner.css;
import com.intellij.flex.uiDesigner.*;
import com.intellij.flex.uiDesigner.io.*;
import com.intellij.flex.uiDesigner.mxml.AmfExtendedTypes;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.javascript.flex.css.FlexCssElementDescriptorProvider;
import com.intellij.javascript.flex.css.FlexCssPropertyDescriptor;
import com.intellij.javascript.flex.css.FlexStyleIndexInfo;
import com.intellij.lang.ASTNode;
import com.intellij.lang.javascript.psi.ecmal4.JSClass;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.css.*;
import com.intellij.psi.css.impl.CssElementTypes;
import com.intellij.psi.css.impl.CssTermTypes;
import com.intellij.psi.css.impl.CssTokenImpl;
import com.intellij.psi.css.impl.util.CssPsiColorUtil;
import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
public class CssWriter {
private static final Logger LOG = Logger.getInstance(CssWriter.class.getName());
protected PrimitiveAmfOutputStream propertyOut;
private final CustomVectorWriter rulesetVectorWriter = new CustomVectorWriter();
private final CustomVectorWriter declarationVectorWriter = new CustomVectorWriter();
protected final StringRegistry.StringWriter stringWriter;
private ProblemsHolder problemsHolder;
private final AssetCounter assetCounter;
public CssWriter(StringRegistry.StringWriter stringWriter, ProblemsHolder problemsHolder, AssetCounter assetCounter) {
this.stringWriter = stringWriter;
this.problemsHolder = problemsHolder;
this.assetCounter = assetCounter;
}
@Nullable
public byte[] write(@NotNull VirtualFile file, @NotNull Module module) {
Document document = FileDocumentManager.getInstance().getDocument(file);
StylesheetFile cssFile = document != null ? (StylesheetFile)PsiDocumentManager.getInstance(module.getProject()).getPsiFile(document) : null;
if (cssFile == null) {
LOG.warn("CSS file is null for " + file.getName());
return null;
}
problemsHolder.setCurrentFile(file);
try {
return write(cssFile, document, module);
}
finally {
problemsHolder.setCurrentFile(null);
}
}
@Nullable
public byte[] write(@NotNull StylesheetFile stylesheetFile, @NotNull Module module) {
problemsHolder.setCurrentFile(stylesheetFile.getVirtualFile());
try {
Document document = PsiDocumentManager.getInstance(module.getProject()).getDocument(stylesheetFile);
if (document == null) {
LOG.warn("Document is null for " + stylesheetFile.getName());
return null;
}
return write(stylesheetFile, document, module);
}
finally {
problemsHolder.setCurrentFile(null);
}
}
@Nullable
private byte[] write(@NotNull StylesheetFile stylesheetFile, @NotNull Document document, @NotNull Module module) {
CssStylesheet stylesheet = stylesheetFile.getStylesheet();
if (stylesheet == null) {
LOG.warn("Stylesheet is null for " + stylesheetFile.getName());
return null;
}
rulesetVectorWriter.prepareIteration();
DocumentWindow documentWindow = document instanceof DocumentWindow ? (DocumentWindow)document : null;
for (CssRuleset ruleset : stylesheet.getRulesets()) {
CssBlock block = ruleset.getBlock();
if (block == null) {
continue;
}
PrimitiveAmfOutputStream rulesetOut = rulesetVectorWriter.getOutputForIteration();
int textOffset = ruleset.getTextOffset();
if (documentWindow == null) {
rulesetOut.writeUInt29(document.getLineNumber(textOffset) + 1);
}
else {
rulesetOut.writeUInt29(documentWindow.injectedToHostLine(document.getLineNumber(textOffset)) + 1);
textOffset = documentWindow.injectedToHost(textOffset);
}
rulesetOut.writeUInt29(textOffset);
writeSelectors(ruleset, rulesetOut, module);
declarationVectorWriter.prepareIteration();
for (CssDeclaration declaration : block.getDeclarations()) {
CssTermList value = declaration.getValue();
if (value == null || PsiTreeUtil.getChildOfType(value, PsiErrorElement.class) != null) {
continue;
}
propertyOut = declarationVectorWriter.getOutputForIteration();
try {
stringWriter.write(declaration.getPropertyName(), propertyOut);
textOffset = declaration.getTextOffset();
propertyOut.writeUInt29(documentWindow == null ? textOffset : documentWindow.injectedToHost(textOffset));
CssPropertyDescriptor propertyDescriptor = ContainerUtil.getFirstItem(declaration.getDescriptors());
writePropertyValue(value, propertyDescriptor != null && propertyDescriptor instanceof FlexCssPropertyDescriptor
? ((FlexCssPropertyDescriptor)propertyDescriptor).getStyleInfo()
: null);
continue;
}
catch (RuntimeException e) {
problemsHolder.add(declaration, e, declaration.getPropertyName());
}
catch (Throwable e) {
problemsHolder.add(e);
}
declarationVectorWriter.rollbackLastIteration();
}
// must be written in any case, IDEA-86219, ruleset without rules
declarationVectorWriter.writeTo(rulesetOut);
}
PrimitiveAmfOutputStream outputForCustomData = rulesetVectorWriter.getOutputForCustomData();
CssNamespace[] namespaces = stylesheet.getNamespaces();
outputForCustomData.write(namespaces.length);
if (namespaces.length > 0) {
for (CssNamespace cssNamespace : namespaces) {
stringWriter.writeNullable(cssNamespace.getPrefix(), outputForCustomData);
stringWriter.writeNullable(cssNamespace.getUri(), outputForCustomData);
}
}
return IOUtil.getBytes(rulesetVectorWriter);
}
private void writeSelectors(@NotNull CssRuleset ruleset, @NotNull PrimitiveAmfOutputStream out, @NotNull Module module) {
CssSelector[] selectors = ruleset.getSelectors();
out.write(selectors.length);
for (CssSelector selector : selectors) {
CssSimpleSelector[] simpleSelectors = selector.getSimpleSelectors();
out.write(simpleSelectors.length);
for (CssSimpleSelector simpleSelector : simpleSelectors) {
// subject
if (simpleSelector.isUniversalSelector()) {
out.write(0);
}
else {
XmlElementDescriptor typeSelectorDescriptor = FlexCssElementDescriptorProvider.getTypeSelectorDescriptor(simpleSelector, module);
String subject = simpleSelector.getElementName();
if (typeSelectorDescriptor == null) {
if (!subject.equals("global")) {
LOG.warn("unqualified type selector " + simpleSelector.getText());
}
stringWriter.write(subject, out);
out.write(0);
}
else {
stringWriter.write(typeSelectorDescriptor.getQualifiedName(), out);
stringWriter.write(subject, out);
stringWriter.writeNullable(simpleSelector.getNamespaceName(), out);
}
}
// conditions
CssSelectorSuffix[] selectorSuffixes = simpleSelector.getSelectorSuffixes();
out.write(selectorSuffixes.length);
for (CssSelectorSuffix selectorSuffix : selectorSuffixes) {
if (selectorSuffix instanceof CssClass) {
out.write(FlexCssConditionKind.CLASS);
}
else if (selectorSuffix instanceof CssIdSelector) {
out.write(FlexCssConditionKind.ID);
}
else if (selectorSuffix instanceof CssPseudoClass) {
out.write(FlexCssConditionKind.PSEUDO);
}
else {
LOG.error("unknown selector suffix " + selectorSuffix.getText());
}
stringWriter.write(selectorSuffix.getName(), out);
}
}
}
}
private void writeStringValue(@NotNull ASTNode node, @Nullable FlexStyleIndexInfo info) {
boolean stripQuotes = node.getElementType() == CssElementTypes.CSS_STRING;
if (stripQuotes) {
node = node.getFirstChildNode();
}
final CharSequence chars = node.getChars();
if (info == null || info.getEnumeration() == null) {
if (stripQuotes) {
propertyOut.write(Amf3Types.STRING);
writeCssStringToken(chars);
}
else {
if (StringUtil.equals(chars, "true")) {
propertyOut.write(Amf3Types.TRUE);
}
else if (StringUtil.equals(chars, "false")) {
propertyOut.write(Amf3Types.FALSE);
}
else {
propertyOut.write(Amf3Types.STRING);
propertyOut.writeAmfUtf(chars);
}
}
}
else {
propertyOut.write(AmfExtendedTypes.STRING_REFERENCE);
stringWriter.write(stripQuotes ? chars.subSequence(1, chars.length() - 1).toString() : chars.toString(), propertyOut);
}
}
private void writeCssStringToken(CharSequence chars) {
propertyOut.writeAmfUtf(chars, false, 1, chars.length() - 1);
}
// In Flex css number cannot be hex (#ddaabb, allowable only for Color)
private void writeNumberValue(ASTNode node, final boolean isInt) {
final IElementType elementType = node.getElementType();
boolean isNegative = false;
if (elementType == CssElementTypes.CSS_NUMBER_TERM) {
node = node.getFirstChildNode();
// todo honor unit, IDEA-72089
}
else if (elementType == CssElementTypes.CSS_MINUS) {
isNegative = true;
node = node.getTreeNext().getFirstChildNode();
}
else if (elementType == CssElementTypes.CSS_HASH) {
final CharSequence chars = node.getChars();
if (chars.length() > (1 + 6)) {
propertyOut.writeAmfUInt(IOUtil.parseLong(chars, 1, false, 16));
}
else {
propertyOut.writeAmfUInt(IOUtil.parseInt(chars, 1, false, 16));
}
return;
}
else if (elementType == CssElementTypes.CSS_IDENT) {
assert StringUtil.equals(node.getChars(), "NaN");
propertyOut.writeAmfDouble(Double.NaN);
return;
}
else {
throw new IllegalArgumentException("unknown number value type " + elementType);
}
IOUtil.writeAmfIntOrDouble(propertyOut, node.getChars(), isNegative, isInt);
}
private static boolean isArray(PsiElement sibling) {
if (sibling == null) {
return false;
}
if (sibling instanceof PsiWhiteSpace) {
sibling = sibling.getNextSibling();
if (sibling == null) {
return false;
}
}
// ignore any other CssTerm if delimited by whitespace, according to flex compiler
return sibling.getNode().getElementType() == CssElementTypes.CSS_COMMA;
}
/**
* If there is no descriptor (FlexCssPropertyDescriptor) for property, then:
* 1) property is outdated and unused, but it have forgotten remove it
* 2) developer is too lazy
*/
private void writePropertyValue(CssTermList value, @Nullable FlexStyleIndexInfo info) throws InvalidPropertyException {
final PsiElement firstChild = value.getFirstChild();
if (firstChild == null) {
throw new InvalidPropertyException(value, "invalid.value");
}
int lengthPosition = -1;
if (isArray(firstChild.getNextSibling())) {
propertyOut.write(Amf3Types.ARRAY);
lengthPosition = propertyOut.size();
// assume array length will be less 128
propertyOut.write(0);
}
boolean writeCssType = lengthPosition == -1;
int length = 0;
boolean expectTerm = true;
for (PsiElement child = value.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof CssTerm) {
if (!expectTerm) {
break;
}
CssTermType termType = ((CssTerm)child).getTermType();
if (termType == CssTermTypes.COLOR) {
if (writeCssType) {
propertyOut.write(CssPropertyType.COLOR_INT);
}
Color color = CssPsiColorUtil.getColor(child);
assert color != null;
propertyOut.writeAmfUInt(color.getRGB());
}
else if (termType == CssTermTypes.IDENT) {
//noinspection ConstantConditions
ASTNode node = child.getFirstChild().getNode();
if (node.getElementType() == CssElementTypes.CSS_FUNCTION) {
LOG.assertTrue(writeCssType);
writeFunctionValue((CssFunction)node, info);
}
else {
writeStringValue(node, info);
}
}
else if (termType == CssTermTypes.NUMBER || termType == CssTermTypes.NEGATIVE_NUMBER || termType == CssTermTypes.LENGTH || termType == CssTermTypes.NEGATIVE_LENGTH) {
// todo if termType equals CssTermTypes.LENGTH, we must respect unit
//noinspection ConstantConditions
writeNumberValue(child.getFirstChild().getNode(), false);
}
else if (termType == CssTermTypes.STRING) {
writeStringValue(child.getFirstChild().getNode(), info);
}
else {
ASTNode node = child.getFirstChild().getNode();
if (node.getElementType() == CssElementTypes.CSS_FUNCTION) {
LOG.assertTrue(writeCssType);
writeFunctionValue((CssFunction)node, info);
}
else {
throw new InvalidPropertyException("unknown css term type: " + termType + " in " + value.getText(), value);
}
}
length++;
expectTerm = false;
}
else if (child.getNode().getElementType() == CssElementTypes.CSS_COMMA) {
if (expectTerm) {
break;
}
expectTerm = true;
}
}
if (lengthPosition != -1) {
assert length < 128;
propertyOut.putByte(length, lengthPosition);
}
}
@SuppressWarnings("ConstantConditions")
private void writeFunctionValue(CssFunction cssFunction, @Nullable FlexStyleIndexInfo info) throws InvalidPropertyException {
final String functionName = cssFunction.getName();
CssTermList termList = cssFunction.getValue();
if (termList == null) {
throw new InvalidPropertyException("termList is null: " + functionName);
}
switch (functionName.charAt(0)) {
case 'C':
writeClassReference(info, termList.getNode().getFirstChildNode().getFirstChildNode());
break;
case 'E':
writeEmbed(cssFunction, termList);
break;
case 'P':
writePropertyReference(termList.getNode().getFirstChildNode().getFirstChildNode());
break;
default:
throw new IllegalArgumentException("unknown function: " + functionName);
}
}
private void writeClassReference(FlexStyleIndexInfo info, ASTNode valueNode) throws InvalidPropertyException {
// ClassReference(null);
if (valueNode instanceof CssTokenImpl) {
assert StringUtil.equals(valueNode.getChars(), "null");
propertyOut.write(Amf3Types.NULL);
}
else {
CssString cssString = (CssString)valueNode;
JSClass jsClass = InjectionUtil.getJsClassFromPackageAndLocalClassNameReferences(cssString);
if (jsClass == null) {
final CharSequence chars = valueNode.getFirstChildNode().getChars();
throw new InvalidPropertyException(cssString, "unresolved.class", chars.subSequence(1, chars.length() - 1));
}
writeClassReference(jsClass, info, cssString);
}
}
private static void writePropertyReference(ASTNode valueNode) throws InvalidPropertyException {
String reference = ((CssString)valueNode).getValue();
throw new InvalidPropertyException(valueNode.getPsi(), "property.reference.is.not.yet.supported", reference);
// it seems FQN access like "al: PropertyReference("spark.layouts.VerticalAlign.TOP")" is not working, only document reference is allowed
// todo
//if () {
//
//}
}
protected void writeClassReference(JSClass jsClass, FlexStyleIndexInfo info, CssString cssString) throws InvalidPropertyException {
propertyOut.write(AmfExtendedTypes.CLASS_REFERENCE);
stringWriter.write(jsClass.getQualifiedName(), propertyOut);
}
private void writeEmbed(CssFunction cssFunction, CssTermList termList) throws InvalidPropertyException {
VirtualFile source = null;
String symbol = null;
String mimeType = null;
for (PsiElement child = termList.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof CssTerm) {
PsiElement firstChild = child.getFirstChild();
if (firstChild instanceof LeafElement && ((LeafElement)firstChild).getElementType() == CssElementTypes.CSS_IDENT) {
CharSequence name = firstChild.getNode().getChars();
@SuppressWarnings("ConstantConditions")
PsiElement valueElement = child.getLastChild().getFirstChild();
if (StringUtil.equals(name, "source")) {
source = InjectionUtil.getReferencedFile(valueElement);
}
else {
String value = ((CssString)valueElement).getValue();
if (StringUtil.equals(name, "symbol")) {
symbol = value;
}
else if (StringUtil.equals(name, "mimeType")) {
mimeType = value;
}
else {
LOG.warn("unsupported embed param: " + name + "=" + value);
}
}
}
else if (firstChild instanceof CssTermList) {
CssTerm[] terms = ((CssTermList)firstChild).getTerms();
if (terms.length == 2) {
CharSequence name = terms[0].getNode().getChars();
if (StringUtil.equals(name, "source")) {
source = InjectionUtil.getReferencedFile(terms[1].getFirstChild());
}
else {
String value = ((CssString)terms[1].getFirstChild()).getValue();
if (StringUtil.equals(name, "symbol")) {
symbol = value;
}
else if (StringUtil.equals(name, "mimeType")) {
mimeType = value;
}
else {
LOG.warn("unsupported embed param: " + name + "=" + value);
}
}
}
else {
LOG.warn("unsupported embed: " + firstChild.getText());
}
}
else if (firstChild instanceof CssString) {
source = InjectionUtil.getReferencedFile(firstChild);
}
else {
LOG.warn("unsupported embed statement: " + cssFunction.getNode().getChars());
}
}
}
if (source == null) {
throw new InvalidPropertyException(cssFunction, FlashUIDesignerBundle.message("embed.source.not.specified", cssFunction.getText()));
}
final int fileId;
final boolean isSwf = InjectionUtil.isSwf(source, mimeType);
if (isSwf) {
fileId = EmbedSwfManager.getInstance().add(source, symbol, assetCounter);
}
else if (InjectionUtil.isImage(source, mimeType)) {
fileId = EmbedImageManager.getInstance().add(source, mimeType, assetCounter);
}
else {
throw new InvalidPropertyException(cssFunction, FlashUIDesignerBundle.message("unsupported.embed.asset.type", cssFunction.getText()));
}
propertyOut.write(isSwf ? AmfExtendedTypes.SWF : AmfExtendedTypes.IMAGE);
propertyOut.writeUInt29(fileId);
}
private static final class FlexCssConditionKind {
public static final int CLASS = 0;
public static final int ID = 1;
public static final int PSEUDO = 2;
}
}