com.intellij.codeInsight.template.emmet.nodes.GenerationNode 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-2015 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.nodes;
import com.google.common.base.Strings;
import com.intellij.application.options.emmet.EmmetOptions;
import com.intellij.codeInsight.template.CustomTemplateCallback;
import com.intellij.codeInsight.template.LiveTemplateBuilder;
import com.intellij.codeInsight.template.emmet.XmlEmmetParser;
import com.intellij.codeInsight.template.emmet.ZenCodingUtil;
import com.intellij.codeInsight.template.emmet.filters.SingleLineEmmetFilter;
import com.intellij.codeInsight.template.emmet.filters.ZenCodingFilter;
import com.intellij.codeInsight.template.emmet.generators.XmlZenCodingGenerator;
import com.intellij.codeInsight.template.emmet.generators.XmlZenCodingGeneratorImpl;
import com.intellij.codeInsight.template.emmet.generators.ZenCodingGenerator;
import com.intellij.codeInsight.template.emmet.tokens.TemplateToken;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.injected.editor.DocumentWindowImpl;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.command.undo.UndoConstants;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.util.HtmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Pattern;
import static com.google.common.collect.Lists.newArrayList;
public class GenerationNode extends UserDataHolderBase {
private final TemplateToken myTemplateToken;
private final List myChildren = newArrayList();
private final int myNumberInIteration;
private final int myTotalIterations;
private String mySurroundedText;
private final boolean myInsertSurroundedTextAtTheEnd;
private final boolean myInsertNewLineBetweenNodes;
private GenerationNode myParent;
private boolean myContainsSurroundedTextMarker = false;
private static final Pattern ATTRIBUTE_VARIABLE_PATTERN = Pattern.compile("\\$[A-z_0-9]+\\$");
private static final Pattern HREF_PATTERN = Pattern.compile("^(?:(?:https?|ftp|file)://|www\\.|ftp\\.)(?:\\([-A-Z0-9+&@#/%=~_|$?!:,.]*\\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\\([-A-Z0-9+&@#/%=~_|$?!:,.]*\\)|[A-Z0-9+&@#/%=~_|$])",
Pattern.CASE_INSENSITIVE);
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-z0-9._%+-]+@[A-z0-9.-]+\\.[A-z]{2,5}$");
private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^([A-z]+:)?//");
public GenerationNode(TemplateToken templateToken,
int numberInIteration,
int totalIterations, String surroundedText,
boolean insertSurroundedTextAtTheEnd, GenerationNode parent) {
this(templateToken, numberInIteration, totalIterations, surroundedText, insertSurroundedTextAtTheEnd, parent, false);
}
public GenerationNode(TemplateToken templateToken,
int numberInIteration,
int totalIterations, String surroundedText,
boolean insertSurroundedTextAtTheEnd, GenerationNode parent, boolean insertNewLineBetweenNodes) {
myTemplateToken = templateToken;
myNumberInIteration = numberInIteration;
myTotalIterations = totalIterations;
mySurroundedText = surroundedText;
myInsertSurroundedTextAtTheEnd = insertSurroundedTextAtTheEnd;
myInsertNewLineBetweenNodes = insertNewLineBetweenNodes;
if(parent != null) {
parent.addChild(this);
}
}
public boolean isInsertNewLineBetweenNodes() {
return myInsertNewLineBetweenNodes;
}
public List getChildren() {
return myChildren;
}
public void addChild(GenerationNode child) {
child.setParent(this);
myChildren.add(child);
}
public void addChildren(Collection children) {
for (GenerationNode child : children) {
addChild(child);
}
}
public boolean isLeaf() {
return myChildren.size() == 0;
}
private boolean isBlockTag() {
if (myTemplateToken != null) {
XmlFile xmlFile = myTemplateToken.getFile();
XmlDocument document = xmlFile.getDocument();
if (document != null) {
XmlTag tag = document.getRootTag();
if (tag != null) {
return HtmlUtil.isHtmlBlockTagL(tag.getName());
}
}
}
return false;
}
@NotNull
public TemplateImpl generate(@NotNull CustomTemplateCallback callback,
@Nullable ZenCodingGenerator generator,
@NotNull Collection filters,
boolean insertSurroundedText, int segmentsLimit) {
myContainsSurroundedTextMarker = !(insertSurroundedText && myInsertSurroundedTextAtTheEnd);
GenerationNode generationNode = this;
if (generationNode != this) {
return generationNode.generate(callback, generator, Collections.emptyList(), insertSurroundedText, segmentsLimit);
}
boolean shouldNotReformatTemplate = false;
boolean oneLineTemplateExpanding = false;
for (ZenCodingFilter filter : filters) {
generationNode = filter.filterNode(generationNode);
if (filter instanceof SingleLineEmmetFilter) {
shouldNotReformatTemplate = true;
oneLineTemplateExpanding = true;
}
}
CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(callback.getProject());
String indentStr;
if (callback.isInInjectedFragment()) {
Editor editor = callback.getEditor();
Document document = editor.getDocument();
if (document instanceof DocumentWindowImpl && ((DocumentWindowImpl)document).isOneLine()) {
/*
* If document is one-line that in the moment of inserting text,
* new line chars will be filtered (see DocumentWindowImpl#insertString).
* So in this case we should filter text by SingleLineAvoid in order to avoid
* inconsistency of template segments.
*/
oneLineTemplateExpanding = true;
filters.add(new SingleLineEmmetFilter());
}
indentStr = "";
}
else if (settings.useTabCharacter(callback.getFileType())) {
indentStr = "\t";
}
else {
int tabSize = settings.getTabSize(callback.getFileType());
indentStr = StringUtil.repeatSymbol(' ', tabSize);
}
LiveTemplateBuilder builder = new LiveTemplateBuilder(segmentsLimit);
int end = -1;
boolean hasChildren = myChildren.size() > 0;
TemplateImpl parentTemplate;
Map predefinedValues;
if (myTemplateToken instanceof TemplateToken && generator instanceof XmlZenCodingGenerator) {
TemplateToken xmlTemplateToken = myTemplateToken;
parentTemplate = invokeXmlTemplate(xmlTemplateToken, callback, generator, hasChildren);
predefinedValues = buildPredefinedValues(xmlTemplateToken.getAttributes(), (XmlZenCodingGenerator)generator, hasChildren);
}
else {
parentTemplate = invokeTemplate(myTemplateToken, hasChildren, callback, generator);
predefinedValues = null;
}
String s = parentTemplate.getString();
for (ZenCodingFilter filter : filters) {
s = filter.filterText(s, myTemplateToken);
}
parentTemplate = parentTemplate.copy();
parentTemplate.setString(s);
final String txt = hasChildren || myContainsSurroundedTextMarker ? null : mySurroundedText;
parentTemplate = expandTemplate(parentTemplate, predefinedValues, txt, segmentsLimit);
int offset = builder.insertTemplate(0, parentTemplate, null);
int newOffset = gotoChild(callback.getProject(), builder.getText(), offset, 0, builder.length());
if (offset < builder.length() && newOffset != offset) {
end = offset;
}
offset = newOffset;
if (end == -1 && offset < builder.length() && myChildren.size() == 0) {
end = offset;
}
LiveTemplateBuilder.Marker marker = offset < builder.length() ? builder.createMarker(offset) : null;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, myChildrenSize = myChildren.size(); i < myChildrenSize; i++) {
GenerationNode child = myChildren.get(i);
TemplateImpl childTemplate = child.generate(callback, generator, filters, !myContainsSurroundedTextMarker, segmentsLimit);
boolean blockTag = child.isBlockTag();
if (!oneLineTemplateExpanding && blockTag && !isNewLineBefore(builder.getText(), offset)) {
builder.insertText(offset, "\n" + indentStr, false);
offset += indentStr.length() + 1;
}
int e = builder.insertTemplate(offset, childTemplate, null);
offset = marker != null ? marker.getEndOffset() : builder.length();
if (!oneLineTemplateExpanding && ((blockTag && !isNewLineAfter(builder.getText(), offset)) || myInsertNewLineBetweenNodes)) {
builder.insertText(offset, "\n" + indentStr, false);
offset += indentStr.length() + 1;
}
if (end == -1 && e < offset) {
end = e;
}
}
if (shouldNotReformatTemplate) {
builder.setIsToReformat(false);
}
return builder.buildTemplate();
}
private static TemplateImpl invokeTemplate(TemplateToken token,
boolean hasChildren,
final CustomTemplateCallback callback,
@Nullable ZenCodingGenerator generator) {
TemplateImpl template = token.getTemplate();
if (generator != null) {
assert template != null;
template = generator.generateTemplate(token, hasChildren, callback.getContext());
removeVariablesWhichHasNoSegment(template);
}
return template;
}
private TemplateImpl invokeXmlTemplate(final TemplateToken token,
CustomTemplateCallback callback,
@Nullable ZenCodingGenerator generator,
final boolean hasChildren) {
/*assert generator == null || generator instanceof XmlZenCodingGenerator :
"The generator cannot process TemplateToken because it doesn't inherit XmlZenCodingGenerator";*/
ZenCodingGenerator zenCodingGenerator = ObjectUtils.notNull(generator, XmlZenCodingGeneratorImpl.INSTANCE);
Map attributes = token.getAttributes();
TemplateImpl template = token.getTemplate();
assert template != null;
final XmlFile xmlFile = token.getFile();
PsiFileFactory fileFactory = PsiFileFactory.getInstance(xmlFile.getProject());
String text = xmlFile.getText();
final PsiElement context = callback.getFile().getContext();
if (context != null && context.getText().startsWith("\"")) {
text = text.replace('"', '\'');
}
XmlFile dummyFile = (XmlFile)fileFactory.createFileFromText("dummy.html", HTMLLanguage.INSTANCE, text, false, true);
final XmlTag tag = dummyFile.getRootTag();
if (tag != null) {
// autodetect href
if (EmmetOptions.getInstance().isHrefAutoDetectEnabled() && StringUtil.isNotEmpty(mySurroundedText)) {
final boolean isEmptyLinkTag = "a".equalsIgnoreCase(tag.getName()) && isEmptyValue(tag.getAttributeValue("href"));
if (!hasChildren && isEmptyLinkTag) {
if (HREF_PATTERN.matcher(mySurroundedText).matches()) {
attributes.put("href", PROTOCOL_PATTERN.matcher(mySurroundedText).find()
? mySurroundedText.trim()
: "http://" + mySurroundedText.trim());
}
else if (EMAIL_PATTERN.matcher(mySurroundedText).matches()) {
attributes.put("href", "mailto:" + mySurroundedText.trim());
}
}
}
for (Map.Entry attribute : attributes.entrySet()) {
if (Strings.isNullOrEmpty(attribute.getValue())) {
template.addVariable(prepareVariableName(attribute.getKey()), "", "", true);
}
}
XmlTag tag1 = hasChildren ? expandEmptyTagIfNecessary(tag) : tag;
setAttributeValues(tag1, attributes, callback, zenCodingGenerator.isHtml(callback));
XmlFile physicalFile = (XmlFile)fileFactory.createFileFromText(HTMLLanguage.INSTANCE, tag1.getContainingFile().getText());
VirtualFile vFile = physicalFile.getVirtualFile();
if (vFile != null) {
vFile.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
}
token.setFile(physicalFile);
}
template = zenCodingGenerator.generateTemplate(token, hasChildren, callback.getContext());
removeVariablesWhichHasNoSegment(template);
return template;
}
private static String prepareVariableName(@NotNull String attributeName) {
char[] toReplace = {'$', '-', '+', ':'};
StringBuilder builder = new StringBuilder(attributeName.length());
for (int i = 0; i < attributeName.length(); i++) {
char c = attributeName.charAt(i);
boolean replaced = false;
for (char aToReplace : toReplace) {
if (c == aToReplace) {
builder.append('_');
replaced = true;
break;
}
}
if (!replaced) {
builder.append(c);
}
}
return builder.toString();
}
@NotNull
private static TemplateImpl expandTemplate(@NotNull TemplateImpl template,
Map predefinedVarValues,
String surroundedText,
int segmentsLimit) {
LiveTemplateBuilder builder = new LiveTemplateBuilder(segmentsLimit);
if (predefinedVarValues == null && surroundedText == null) {
return template;
}
int offset = builder.insertTemplate(0, template, predefinedVarValues);
if (surroundedText != null) {
builder.insertText(offset, surroundedText, true);
builder.setIsToReformat(true);
}
return builder.buildTemplate();
}
@NotNull
private static XmlTag expandEmptyTagIfNecessary(@NotNull XmlTag tag) {
StringBuilder builder = new StringBuilder();
boolean flag = false;
for (PsiElement child : tag.getChildren()) {
if (child instanceof XmlToken && XmlTokenType.XML_EMPTY_ELEMENT_END.equals(((XmlToken)child).getTokenType())) {
flag = true;
break;
}
builder.append(child.getText());
}
if (flag) {
builder.append(">").append(tag.getName()).append('>');
return XmlElementFactory.getInstance(tag.getProject()).createTagFromText(builder.toString(), XMLLanguage.INSTANCE);
}
return tag;
}
private static int gotoChild(Project project, CharSequence text, int offset, int start, int end) {
PsiFile file = PsiFileFactory.getInstance(project)
.createFileFromText("dummy.xml", StdFileTypes.XML, text, LocalTimeCounter.currentTime(), false);
PsiElement element = file.findElementAt(offset);
if (offset < end && element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
return offset;
}
int newOffset = -1;
XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, start, end, XmlTag.class);
if (tag != null) {
for (PsiElement child : tag.getChildren()) {
if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
newOffset = child.getTextOffset();
}
}
}
if (newOffset >= 0) {
return newOffset;
}
return offset;
}
private static void removeVariablesWhichHasNoSegment(TemplateImpl template) {
Set segments = new HashSet();
for (int i = 0; i < template.getSegmentsCount(); i++) {
segments.add(template.getSegmentName(i));
}
for (int i = template.getVariableCount() - 1; i >= 0; i--) {
String varName = template.getVariableNameAt(i);
if (!segments.contains(varName)) {
template.removeVariable(i);
}
else {
segments.remove(varName);
}
}
}
@Nullable
private Map buildPredefinedValues(@NotNull Map attributes,
@Nullable XmlZenCodingGenerator generator,
boolean hasChildren) {
if (generator == null) {
return Collections.emptyMap();
}
for (String value : attributes.values()) {
if (ZenCodingUtil.containsSurroundedTextMarker(value)) {
myContainsSurroundedTextMarker = true;
break;
}
}
String attributesString = generator.buildAttributesString(attributes, hasChildren, myNumberInIteration, myTotalIterations, mySurroundedText);
attributesString = attributesString.length() > 0 ? ' ' + attributesString : null;
Map predefinedValues = null;
if (attributesString != null) {
predefinedValues = new HashMap();
predefinedValues.put(TemplateToken.ATTRS, attributesString);
}
return predefinedValues;
}
private void setAttributeValues(@NotNull XmlTag tag,
@NotNull final Map attributes,
@NotNull CustomTemplateCallback callback,
boolean isHtml) {
// default and implied attributes
final String defaultAttributeValue = attributes.get(XmlEmmetParser.DEFAULT_ATTRIBUTE_NAME);
if (defaultAttributeValue != null) {
attributes.remove(XmlEmmetParser.DEFAULT_ATTRIBUTE_NAME);
// exclude user defined attributes
final List xmlAttributes = ContainerUtil.filter(tag.getAttributes(), new Condition() {
@Override
public boolean value(XmlAttribute attribute) {
return !attributes.containsKey(attribute.getLocalName());
}
});
XmlAttribute defaultAttribute = findDefaultAttribute(xmlAttributes);
if (defaultAttribute == null) {
defaultAttribute = findImpliedAttribute(xmlAttributes);
}
if (defaultAttribute == null) {
defaultAttribute = findEmptyAttribute(xmlAttributes);
}
if (defaultAttribute != null) {
String attributeName = defaultAttribute.getName();
if (attributeName.length() > 1) {
if (isImpliedAttribute(attributeName) || isDefaultAttribute(attributeName)) {
defaultAttribute.setName(attributeName.substring(1));
}
final String oldValue = defaultAttribute.getValue();
if (oldValue != null && StringUtil.containsChar(oldValue, '|')) {
defaultAttribute.setValue(StringUtil.replace(oldValue, "|", defaultAttributeValue));
}
else {
defaultAttribute.setValue(defaultAttributeValue);
}
}
}
}
// boolean attributes
for (XmlAttribute xmlAttribute : tag.getAttributes()) {
final String attributeName = xmlAttribute.getName();
final XmlAttributeValue xmlAttributeValueElement = xmlAttribute.getValueElement();
if ((xmlAttributeValueElement != null && !attributes.containsKey(attributeName)) || !ZenCodingUtil.isXML11ValidQName(attributeName)) {
continue;
}
String attributeValue = StringUtil.notNullize(attributes.get(attributeName), StringUtil.notNullize(xmlAttribute.getValue()));
if (ZenCodingUtil.containsSurroundedTextMarker(attributeValue)) {
myContainsSurroundedTextMarker = true;
}
if (isHtml && isBooleanAttribute(attributeValue, xmlAttribute, callback)) {
if (HtmlUtil.isShortNotationOfBooleanAttributePreferred()) {
if (xmlAttributeValueElement != null) {
final PsiElement prevSibling = xmlAttributeValueElement.getPrevSibling();
if (prevSibling != null && prevSibling.textMatches("=")) {
xmlAttribute.deleteChildRange(prevSibling, xmlAttributeValueElement);
}
}
}
else {
if (xmlAttributeValueElement == null) {
xmlAttribute.delete();
}
tag.setAttribute(attributeName, attributeName);
}
}
else {
if (xmlAttributeValueElement == null) {
xmlAttribute.delete();
}
tag.setAttribute(attributeName, StringUtil.isEmpty(attributeValue)
? "$" + prepareVariableName(attributeName) + "$"
: ZenCodingUtil.getValue(attributeValue, myNumberInIteration, myTotalIterations, mySurroundedText));
}
}
// remove all implicit and default attributes
for (XmlAttribute xmlAttribute : tag.getAttributes()) {
final String xmlAttributeLocalName = xmlAttribute.getLocalName();
if (isImpliedAttribute(xmlAttributeLocalName) || isDefaultAttribute(xmlAttributeLocalName)) {
xmlAttribute.delete();
}
}
}
private static boolean isBooleanAttribute(@Nullable String attributeValue,
@NotNull XmlAttribute xmlAttribute,
@NotNull CustomTemplateCallback callback) {
if (XmlEmmetParser.BOOLEAN_ATTRIBUTE_VALUE.equals(attributeValue)) {
return true;
}
if (StringUtil.isEmpty(attributeValue)) {
final XmlAttributeDescriptor descriptor = xmlAttribute.getDescriptor();
return descriptor != null && HtmlUtil.isBooleanAttribute(descriptor, callback.getContext());
}
return false;
}
private static boolean isDefaultAttribute(String xmlAttributeLocalName) {
return StringUtil.startsWithChar(xmlAttributeLocalName, '@');
}
private static boolean isImpliedAttribute(String xmlAttributeLocalName) {
return StringUtil.startsWithChar(xmlAttributeLocalName, '!');
}
private static boolean isEmptyValue(String attributeValue) {
return StringUtil.isEmpty(attributeValue) || ATTRIBUTE_VARIABLE_PATTERN.matcher(attributeValue).matches();
}
@Nullable
private static XmlAttribute findDefaultAttribute(@NotNull List attributes) {
for (XmlAttribute attribute : attributes) {
if (isDefaultAttribute(attribute.getLocalName())) {
return attribute;
}
}
return null;
}
@Nullable
private static XmlAttribute findImpliedAttribute(@NotNull List attributes) {
for (XmlAttribute attribute : attributes) {
if (isImpliedAttribute(attribute.getLocalName())) {
return attribute;
}
}
return null;
}
@Nullable
private static XmlAttribute findEmptyAttribute(@NotNull List attributes) {
for (XmlAttribute attribute : attributes) {
final String attributeValue = attribute.getValue();
if (isEmptyValue(attributeValue)) {
return attribute;
}
}
return null;
}
private static boolean isNewLineBefore(CharSequence text, int offset) {
int i = offset - 1;
while (i >= 0 && Character.isWhitespace(text.charAt(i))) {
if (text.charAt(i) == '\n') {
return true;
}
i--;
}
return i < 0;
}
private static boolean isNewLineAfter(CharSequence text, int offset) {
int i = offset;
while (i < text.length() && Character.isWhitespace(text.charAt(i))) {
if (text.charAt(i) == '\n') {
return true;
}
i++;
}
return i == text.length();
}
public TemplateToken getTemplateToken() {
return myTemplateToken;
}
public String getSurroundedText() {
return mySurroundedText;
}
public void setSurroundedText(String surroundedText) {
mySurroundedText = surroundedText;
}
public GenerationNode getParent() {
return myParent;
}
public void setParent(GenerationNode parent) {
myParent = parent;
}
}