com.intellij.codeInsight.template.emmet.XmlEmmetParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xml Show documentation
Show all versions of xml Show documentation
A packaging of the IntelliJ Community Edition xml library.
This is release number 1 of trunk branch 142.
The newest version!
/*
* 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.lang.StdLanguages;
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.ContainerUtil;
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;
public class XmlEmmetParser extends EmmetParser {
public static final String DEFAULT_ATTRIBUTE_NAME = "%default";
public static final String BOOLEAN_ATTRIBUTE_VALUE = "%boolean";
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 boolean hasTagContext = false;
private final Stack tagLevel = new Stack();
private static final Map parentChildTagMapping = new HashMap() {{
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");
}};
private boolean isHtml;
public XmlEmmetParser(List 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);
}
isHtml = generator.isHtml(callback);
if (parentTag != null) {
hasTagContext = true;
tagLevel.push(parentTag.getName());
}
}
@Nullable
private String parseAttributeName() {
String name = "";
ZenCodingToken token = getToken();
while (token != null) {
if ((token instanceof IdentifierToken)) {
name += ((IdentifierToken)token).getText();
}
else if (token instanceof OperationToken &&
(((OperationToken)token).getSign() == '+' || ((OperationToken)token).getSign() == '-')) {
name += ((OperationToken)token).getSign();
}
else {
break;
}
advance();
token = getToken();
}
if (name.isEmpty()) {
return null;
}
final XmlTag tag = XmlElementFactory.getInstance(myCallback.getProject()).createTagFromText(" ", StdLanguages.HTML);
XmlAttribute[] attributes = tag.getAttributes();
if (attributes.length == 1) {
return attributes[0].getName();
}
else {
return null;
}
}
@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 Map attributes = parseSelectors();
if (mustHaveSelector && attributes.isEmpty()) {
return null;
}
final TemplateToken templateToken = new TemplateToken(templateKey, attributes);
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 isHtml ? 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 Map attributes = parseSelectors();
ZenCodingToken token = getToken();
boolean isRepeating = token instanceof OperationToken && ((OperationToken)token).getSign() == '*';
if (!attributes.isEmpty() || isRepeating) {
String wrapTag = suggestTagName();
TemplateImpl template = myCallback.findApplicableTemplate(templateKey);
if (template == null && !ZenCodingUtil.isXML11ValidQName(templateKey)) {
return null;
}
final TemplateToken templateToken = new TemplateToken(wrapTag, attributes);
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();
}
}
@NotNull
private Map parseSelectors() {
final Map result = ContainerUtil.newLinkedHashMap();
List> attrList = parseSelector();
while (attrList != null) {
for (Couple attr : attrList) {
if (getClassAttributeName().equals(attr.first)) {
result.put(getClassAttributeName(), (StringUtil.notNullize(result.get(getClassAttributeName())) + " " + attr.second).trim());
}
else if (HtmlUtil.ID_ATTRIBUTE_NAME.equals(attr.first)) {
result.put(HtmlUtil.ID_ATTRIBUTE_NAME, (StringUtil.notNullize(result.get(HtmlUtil.ID_ATTRIBUTE_NAME)) + " " + attr.second).trim());
}
else {
result.put(attr.first, attr.second);
}
}
attrList = parseSelector();
}
return result;
}
@Nullable
private List> parseSelector() {
ZenCodingToken token = getToken();
if (token == ZenCodingTokens.OPENING_SQ_BRACKET) {
advance();
final List> 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 ? getClassAttributeName() : HtmlUtil.ID_ATTRIBUTE_NAME;
advance();
token = getToken();
final String value = getAttributeValueByToken(token);
if (!value.isEmpty()) {
advance();
}
return Collections.singletonList(Couple.of(name, value));
}
return null;
}
@NotNull
protected String getClassAttributeName() {
return HtmlUtil.CLASS_ATTRIBUTE_NAME;
}
@Nullable
private List> parseAttributeList() {
final List> result = new ArrayList>();
while (true) {
final Couple 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 parseAttribute() {
final int position = getCurrentPosition();
String attributeName = parseAttributeName();
if (attributeName != null && !attributeName.isEmpty()) {
if (getToken() == ZenCodingTokens.DOT) {
if (isEndOfAttribute(nextToken(1))) {
// boolean attribute
advance(); // dot
return Couple.of(attributeName, BOOLEAN_ATTRIBUTE_VALUE);
}
}
else {
// attribute with value
if (getToken() == ZenCodingTokens.EQ) {
advance();
return Couple.of(attributeName, parseAttributeValue());
}
else {
return Couple.of(attributeName, "");
}
}
}
restorePosition(position);
final String impliedValue = parseAttributeValue();
if (!impliedValue.isEmpty()) {
// implied attribute
return Couple.of(DEFAULT_ATTRIBUTE_NAME, impliedValue);
}
return null;
}
@NotNull
private String parseAttributeValue() {
ZenCodingToken token;
final StringBuilder attrValueBuilder = new StringBuilder();
String value;
do {
token = getToken();
value = getAttributeValueByToken(token);
attrValueBuilder.append(value);
if (!isEndOfAttribute(token)) {
advance();
}
}
while (!isEndOfAttribute(token));
return attrValueBuilder.toString();
}
private static boolean isEndOfAttribute(@Nullable ZenCodingToken nextToken) {
return nextToken == null || nextToken == ZenCodingTokens.SPACE || nextToken == ZenCodingTokens.CLOSING_SQ_BRACKET
|| nextToken == ZenCodingTokens.COMMA;
}
}