blob: 01ddc3ca563f79ba5790008907abf45d71fafa59 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.template.emmet;
import com.google.common.base.Strings;
import com.intellij.codeInsight.template.CustomTemplateCallback;
import com.intellij.codeInsight.template.emmet.generators.ZenCodingGenerator;
import com.intellij.codeInsight.template.emmet.nodes.*;
import com.intellij.codeInsight.template.emmet.tokens.*;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.util.containers.Stack;
import com.intellij.xml.util.HtmlUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.openapi.util.text.StringUtil.startsWithIgnoreCase;
/**
* User: zolotov
* Date: 2/7/13
*/
public class XmlEmmetParser extends EmmetParser {
private static final String DEFAULT_TAG = "div";
private static final int DEFAULT_LOREM_LENGTH = 30;
private static final Pattern LOREM_PATTERN = Pattern.compile("(lorem|lipsum)(\\d*)");
@NonNls private static final String DEFAULT_INLINE_TAG = "span";
@NonNls private static final String LOREM_KEYWORD = "lorem";
@NonNls private static final String LIPSUM_KEYWORD = "lipsum";
private static final String ID = "id";
private static final String CLASS = "class";
private boolean hasTagContext = false;
private final Stack<String> tagLevel = new Stack<String>();
private static final Map<String, String> parentChildTagMapping = new HashMap<String, String>() {{
put("p", "span");
put("ul", "li");
put("ol", "li");
put("table", "tr");
put("tr", "td");
put("tbody", "tr");
put("thead", "tr");
put("tfoot", "tr");
put("colgroup", "col");
put("select", "option");
put("optgroup", "option");
put("audio", "source");
put("video", "source");
put("object", "param");
put("map", "area");
}};
public XmlEmmetParser(List<ZenCodingToken> tokens,
CustomTemplateCallback callback,
ZenCodingGenerator generator, boolean surroundWithTemplate) {
super(tokens, callback, generator);
PsiElement context = callback.getContext();
XmlTag parentTag = PsiTreeUtil.getParentOfType(context, XmlTag.class);
if (surroundWithTemplate && parentTag != null && context.getNode().getElementType() == XmlTokenType.XML_START_TAG_START) {
parentTag = PsiTreeUtil.getParentOfType(parentTag, XmlTag.class);
}
if (parentTag != null) {
hasTagContext = true;
tagLevel.push(parentTag.getName());
}
}
@NotNull
private static String getAttributeValueByToken(@Nullable ZenCodingToken token) {
if (token == null) {
return "";
}
if (token instanceof StringLiteralToken) {
final String text = ((StringLiteralToken)token).getText();
return text.substring(1, text.length() - 1);
}
else if (token instanceof TextToken) {
return ((TextToken)token).getText();
}
else if (token instanceof IdentifierToken) {
return ((IdentifierToken)token).getText();
}
else if (token instanceof NumberToken) {
return Integer.toString(((NumberToken)token).getNumber());
}
else if (token == ZenCodingTokens.DOT || token == ZenCodingTokens.SHARP) {
return token.toString();
}
return "";
}
@Nullable
@Override
protected ZenCodingNode parseTemplate() {
ZenCodingToken token = getToken();
String templateKey = getDefaultTemplateKey();
boolean mustHaveSelector = true;
if (token instanceof IdentifierToken) {
templateKey = ((IdentifierToken)token).getText();
advance();
if (startsWithIgnoreCase(templateKey, LOREM_KEYWORD) || startsWithIgnoreCase(templateKey, LIPSUM_KEYWORD)) {
return parseLorem(templateKey);
}
mustHaveSelector = false;
}
if (templateKey == null) {
return null;
}
TemplateImpl template = myCallback.findApplicableTemplate(templateKey);
if (template == null && !ZenCodingUtil.isXML11ValidQName(templateKey) && !StringUtil.containsChar(templateKey, '$')) {
return null;
}
final List<Couple<String>> attrList = parseSelectors();
if (mustHaveSelector && attrList.size() == 0) {
return null;
}
final TemplateToken templateToken = new TemplateToken(templateKey, attrList);
if (!setTemplate(templateToken, template)) {
return null;
}
return new TemplateNode(templateToken, myGenerator);
}
@Override
protected ZenCodingNode parseClimbUpOperation(@Nullable ZenCodingNode leftPart) {
popTagLevel();
return super.parseClimbUpOperation(leftPart);
}
@Override
protected ZenCodingNode parseMoreOperation(@Nullable ZenCodingNode leftPart) {
String parentTag = getParentTag(leftPart);
boolean hasParent = false;
if (!Strings.isNullOrEmpty(parentTag)) {
hasParent = true;
tagLevel.push(parentTag);
}
ZenCodingNode result = super.parseMoreOperation(leftPart);
if (result == null) {
return null;
}
if (hasParent) {
popTagLevel();
}
return result;
}
@Nullable
private String getDefaultTemplateKey() {
return ZenCodingUtil.isHtml(myCallback) ? suggestTagName() : null;
}
@Nullable
private static String getParentTag(ZenCodingNode node) {
if (node instanceof TemplateNode) {
return ((TemplateNode)node).getTemplateToken().getKey();
}
else if (node instanceof MulOperationNode) {
ZenCodingNode leftOperand = ((MulOperationNode)node).getLeftOperand();
if (leftOperand instanceof TemplateNode) {
return ((TemplateNode)leftOperand).getTemplateToken().getKey();
}
}
return null;
}
@Nullable
private ZenCodingNode parseLorem(String templateKey) {
Matcher matcher = LOREM_PATTERN.matcher(templateKey);
if (matcher.matches()) {
int loremWordsCount = DEFAULT_LOREM_LENGTH;
if (matcher.groupCount() > 1) {
String group = matcher.group(2);
loremWordsCount = group == null || group.isEmpty() ? DEFAULT_LOREM_LENGTH : Integer.parseInt(group);
}
final List<Couple<String>> attrList = parseSelectors();
ZenCodingToken token = getToken();
boolean isRepeating = token instanceof OperationToken && ((OperationToken)token).getSign() == '*';
if (!attrList.isEmpty() || isRepeating) {
String wrapTag = suggestTagName();
TemplateImpl template = myCallback.findApplicableTemplate(templateKey);
if (template == null && !ZenCodingUtil.isXML11ValidQName(templateKey)) {
return null;
}
final TemplateToken templateToken = new TemplateToken(wrapTag, attrList);
if (!setTemplate(templateToken, template)) {
return null;
}
return new MoreOperationNode(new TemplateNode(templateToken), new LoremNode(loremWordsCount));
}
else {
return new LoremNode(loremWordsCount);
}
}
else {
return null;
}
}
private String suggestTagName() {
if (!tagLevel.empty()) {
String parentTag = tagLevel.peek();
if (parentChildTagMapping.containsKey(parentTag)) {
return parentChildTagMapping.get(parentTag);
}
if (HtmlUtil.isPossiblyInlineTag(parentTag)) {
return DEFAULT_INLINE_TAG;
}
}
return DEFAULT_TAG;
}
private void popTagLevel() {
if (tagLevel.size() > (hasTagContext ? 1 : 0)) {
tagLevel.pop();
}
}
@SuppressWarnings("unchecked")
@NotNull
private List<Couple<String>> parseSelectors() {
final List<Couple<String>> result = new ArrayList<Couple<String>>();
int classAttrPosition = -1;
int idAttrPosition = -1;
final StringBuilder classAttrBuilder = new StringBuilder();
final StringBuilder idAttrBuilder = new StringBuilder();
while (true) {
final List<Couple<String>> attrList = parseSelector();
if (attrList == null) {
if (classAttrPosition != -1) {
result.set(classAttrPosition, Couple.of(CLASS, classAttrBuilder.toString()));
}
if (idAttrPosition != -1) {
result.set(idAttrPosition, Couple.of(ID, idAttrBuilder.toString()));
}
return result;
}
for (Couple<String> attr : attrList) {
if (CLASS.equals(attr.first)) {
if (classAttrBuilder.length() > 0) {
classAttrBuilder.append(' ');
}
classAttrBuilder.append(attr.second);
if (classAttrPosition == -1) {
classAttrPosition = result.size();
result.add(attr);
}
}
else if (ID.equals(attr.first)) {
if (idAttrBuilder.length() > 0) {
idAttrBuilder.append(' ');
}
idAttrBuilder.append(attr.second);
if (idAttrPosition == -1) {
idAttrPosition = result.size();
result.add(attr);
}
}
else {
result.add(attr);
}
}
}
}
@Nullable
private List<Couple<String>> parseSelector() {
ZenCodingToken token = getToken();
if (token == ZenCodingTokens.OPENING_SQ_BRACKET) {
advance();
final List<Couple<String>> attrList = parseAttributeList();
if (attrList == null || getToken() != ZenCodingTokens.CLOSING_SQ_BRACKET) {
return null;
}
advance();
return attrList;
}
if (token == ZenCodingTokens.DOT || token == ZenCodingTokens.SHARP) {
final String name = token == ZenCodingTokens.DOT ? CLASS : ID;
advance();
token = getToken();
final String value = getAttributeValueByToken(token);
if (!value.isEmpty()) {
advance();
}
return Collections.singletonList(Couple.of(name, value));
}
return null;
}
@Nullable
private List<Couple<String>> parseAttributeList() {
final List<Couple<String>> result = new ArrayList<Couple<String>>();
while (true) {
final Couple<String> attribute = parseAttribute();
if (attribute == null) {
return result;
}
result.add(attribute);
final ZenCodingToken token = getToken();
if (token != ZenCodingTokens.COMMA && token != ZenCodingTokens.SPACE) {
return result;
}
advance();
}
}
@Nullable
private Couple<String> parseAttribute() {
ZenCodingToken token = getToken();
if (!(token instanceof IdentifierToken)) {
return null;
}
String name = ((IdentifierToken)token).getText();
if (name.isEmpty()) {
return null;
}
final XmlTag tag = XmlElementFactory.getInstance(myCallback.getProject()).createTagFromText("<tag " + name + "=''/>");
XmlAttribute[] attributes = tag.getAttributes();
if (attributes.length == 1) {
name = attributes[0].getName();
}
else {
return null;
}
advance();
token = getToken();
if (token != ZenCodingTokens.EQ) {
return Couple.of(name, "");
}
advance();
final StringBuilder attrValueBuilder = new StringBuilder();
String value;
do {
token = getToken();
value = getAttributeValueByToken(token);
attrValueBuilder.append(value);
if (token != null && token != ZenCodingTokens.CLOSING_SQ_BRACKET
&& token != ZenCodingTokens.SPACE && token != ZenCodingTokens.COMMA) {
advance();
}
}
while (token != null && token != ZenCodingTokens.CLOSING_SQ_BRACKET
&& token != ZenCodingTokens.SPACE && token != ZenCodingTokens.COMMA);
return Couple.of(name, attrValueBuilder.toString());
}
}