fix assertion (EA-20597)
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / template / zencoding / XmlZenCodingTemplate.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.codeInsight.template.zencoding;
17
18 import com.intellij.codeInsight.template.CustomLiveTemplate;
19 import com.intellij.codeInsight.template.CustomTemplateCallback;
20 import com.intellij.codeInsight.template.TemplateInvokationListener;
21 import com.intellij.codeInsight.template.impl.TemplateImpl;
22 import com.intellij.lang.xml.XMLLanguage;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.editor.Editor;
25 import com.intellij.openapi.fileTypes.FileType;
26 import com.intellij.openapi.fileTypes.StdFileTypes;
27 import com.intellij.openapi.util.Pair;
28 import com.intellij.psi.PsiElement;
29 import com.intellij.psi.PsiFile;
30 import com.intellij.psi.PsiFileFactory;
31 import com.intellij.psi.XmlElementFactory;
32 import com.intellij.psi.util.PsiTreeUtil;
33 import com.intellij.psi.xml.*;
34 import com.intellij.util.LocalTimeCounter;
35 import com.intellij.util.containers.HashSet;
36 import com.intellij.xml.util.HtmlUtil;
37 import org.apache.xerces.util.XML11Char;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.util.ArrayList;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.Set;
45
46 /**
47  * @author Eugene.Kudelevsky
48  */
49 public class XmlZenCodingTemplate extends ZenCodingTemplate {
50   private static final String SELECTORS = ".#[";
51   private static final String ID = "id";
52   private static final String CLASS = "class";
53   private static final String DEFAULT_TAG = "div";
54
55   private static String getPrefix(@NotNull String templateKey) {
56     for (int i = 0, n = templateKey.length(); i < n; i++) {
57       char c = templateKey.charAt(i);
58       if (SELECTORS.indexOf(c) >= 0) {
59         return templateKey.substring(0, i);
60       }
61     }
62     return templateKey;
63   }
64
65   @Nullable
66   private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
67     int eqIndex = text.indexOf('=');
68     if (eqIndex > 0) {
69       return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
70     }
71     return null;
72   }
73
74   @Nullable
75   private static XmlTemplateToken parseSelectors(@NotNull String text) {
76     String templateKey = null;
77     List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
78     Set<String> definedAttrs = new HashSet<String>();
79     final List<String> classes = new ArrayList<String>();
80     StringBuilder builder = new StringBuilder();
81     char lastDelim = 0;
82     text += MARKER;
83     int classAttrPosition = -1;
84     for (int i = 0, n = text.length(); i < n; i++) {
85       char c = text.charAt(i);
86       if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
87         if (c != ']') {
88           switch (lastDelim) {
89             case 0:
90               templateKey = builder.toString();
91               break;
92             case '#':
93               if (!definedAttrs.add(ID)) {
94                 return null;
95               }
96               attributes.add(new Pair<String, String>(ID, builder.toString()));
97               break;
98             case '.':
99               if (builder.length() <= 0) {
100                 return null;
101               }
102               if (classAttrPosition < 0) {
103                 classAttrPosition = attributes.size();
104               }
105               classes.add(builder.toString());
106               break;
107             case ']':
108               if (builder.length() > 0) {
109                 return null;
110               }
111               break;
112             default:
113               return null;
114           }
115         }
116         else if (lastDelim != '[') {
117           return null;
118         }
119         else {
120           Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
121           if (pair == null || !definedAttrs.add(pair.first)) {
122             return null;
123           }
124           attributes.add(pair);
125         }
126         lastDelim = c;
127         builder = new StringBuilder();
128       }
129       else {
130         builder.append(c);
131       }
132     }
133     if (classes.size() > 0) {
134       if (definedAttrs.contains(CLASS)) {
135         return null;
136       }
137       StringBuilder classesAttrValue = new StringBuilder();
138       for (int i = 0; i < classes.size(); i++) {
139         classesAttrValue.append(classes.get(i));
140         if (i < classes.size() - 1) {
141           classesAttrValue.append(' ');
142         }
143       }
144       assert classAttrPosition >= 0;
145       attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
146     }
147     return new XmlTemplateToken(templateKey, attributes);
148   }
149
150   private static boolean isXML11ValidQName(String str) {
151     final int colon = str.indexOf(':');
152     if (colon == 0 || colon == str.length() - 1) {
153       return false;
154     }
155     if (colon > 0) {
156       final String prefix = str.substring(0, colon);
157       final String localPart = str.substring(colon + 1);
158       return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
159     }
160     return XML11Char.isXML11ValidNCName(str);
161   }
162
163   public static boolean isTrueXml(CustomTemplateCallback callback) {
164     return isTrueXml(callback.getFileType());
165   }
166
167   public static boolean isTrueXml(FileType type) {
168     return type == StdFileTypes.XHTML || type == StdFileTypes.JSPX || type == StdFileTypes.XML;
169   }
170
171   private static boolean isHtml(CustomTemplateCallback callback) {
172     FileType type = callback.getFileType();
173     return type == StdFileTypes.HTML || type == StdFileTypes.XHTML;
174   }
175
176   @Override
177   @Nullable
178   protected TemplateToken parseTemplateKey(String key, CustomTemplateCallback callback) {
179     String prefix = getPrefix(key);
180     boolean useDefaultTag = false;
181     if (prefix.length() == 0) {
182       if (!isHtml(callback)) {
183         return null;
184       }
185       else {
186         useDefaultTag = true;
187         prefix = DEFAULT_TAG;
188         key = prefix + key;
189       }
190     }
191     TemplateImpl template = callback.findApplicableTemplate(prefix);
192     if (template == null && !isXML11ValidQName(prefix)) {
193       return null;
194     }
195     final XmlTemplateToken token = parseSelectors(key);
196     if (token == null) {
197       return null;
198     }
199     if (useDefaultTag && token.getAttribute2Value().size() == 0) {
200       return null;
201     }
202     if (template == null) {
203       template = generateTagTemplate(token.getKey(), callback);
204     }
205     assert prefix.equals(token.getKey());
206     token.setTemplate(template);
207     final XmlTag tag = parseXmlTagInTemplate(template.getString(), callback, true);
208     if (token.getAttribute2Value().size() > 0 && tag == null) {
209       return null;
210     }
211     if (tag != null) {
212       if (!XmlZenCodingInterpreter.containsAttrsVar(template) && token.getAttribute2Value().size() > 0) {
213         ApplicationManager.getApplication().runWriteAction(new Runnable() {
214           public void run() {
215             addMissingAttributes(tag, token.getAttribute2Value());
216           }
217         });
218       }
219       token.setTag(tag);
220     }
221     return token;
222   }
223
224   private static void addMissingAttributes(XmlTag tag, List<Pair<String, String>> value) {
225     List<Pair<String, String>> attr2value = new ArrayList<Pair<String, String>>(value);
226     for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
227       Pair<String, String> pair = iterator.next();
228       if (tag.getAttribute(pair.first) != null) {
229         iterator.remove();
230       }
231     }
232     addAttributesBefore(tag, attr2value);
233   }
234
235   private static void addAttributesBefore(XmlTag tag, List<Pair<String, String>> attr2value) {
236     XmlAttribute[] attributes = tag.getAttributes();
237     XmlAttribute firstAttribute = attributes.length > 0 ? attributes[0] : null;
238     XmlElementFactory factory = XmlElementFactory.getInstance(tag.getProject());
239     for (Pair<String, String> pair : attr2value) {
240       XmlAttribute xmlAttribute = factory.createXmlAttribute(pair.first, "");
241       if (firstAttribute != null) {
242         tag.addBefore(xmlAttribute, firstAttribute);
243       }
244       else {
245         tag.add(xmlAttribute);
246       }
247     }
248   }
249
250   @NotNull
251   private static TemplateImpl generateTagTemplate(String tagName, CustomTemplateCallback callback) {
252     StringBuilder builder = new StringBuilder("<");
253     builder.append(tagName).append('>');
254     if (isTrueXml(callback) || !HtmlUtil.isSingleHtmlTag(tagName)) {
255       builder.append("$END$</").append(tagName).append('>');
256     }
257     return new TemplateImpl("", builder.toString(), "");
258   }
259
260   @Nullable
261   static XmlTag parseXmlTagInTemplate(String templateString, CustomTemplateCallback callback, boolean createPhysicalFile) {
262     XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(callback.getProject())
263       .createFileFromText("dummy.xml", StdFileTypes.XML, templateString, LocalTimeCounter.currentTime(), createPhysicalFile);
264     XmlDocument document = xmlFile.getDocument();
265     return document == null ? null : document.getRootTag();
266   }
267
268   protected boolean isApplicable(@NotNull PsiElement element) {
269     if (element.getLanguage() instanceof XMLLanguage) {
270       if (PsiTreeUtil.getParentOfType(element, XmlAttributeValue.class) != null) {
271         return false;
272       }
273       if (PsiTreeUtil.getParentOfType(element, XmlComment.class) != null) {
274         return false;
275       }
276       if (!findApplicableFilter(element)) {
277         return false;
278       }
279       return true;
280     }
281     return false;
282   }
283
284   private static boolean findApplicableFilter(@NotNull PsiElement context) {
285     for (ZenCodingFilter filter : ZenCodingFilter.EP_NAME.getExtensions()) {
286       if (filter.isMyContext(context)) {
287         return true;
288       }
289     }
290     return new XmlZenCodingFilterImpl().isMyContext(context);
291   }
292
293   public static boolean startZenCoding(Editor editor, PsiFile file, String abbreviation) {
294     int caretAt = editor.getCaretModel().getOffset();
295     XmlZenCodingTemplate template = CustomLiveTemplate.EP_NAME.findExtension(XmlZenCodingTemplate.class);
296     if (abbreviation != null && !template.supportsWrapping()) {
297       return false;
298     }
299     if (template.isApplicable(file, caretAt)) {
300       final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
301       if (abbreviation != null) {
302         String selection = callback.getEditor().getSelectionModel().getSelectedText();
303         assert selection != null;
304         selection = selection.trim();
305         template.doWrap(selection, abbreviation, callback, new TemplateInvokationListener() {
306           public void finished() {
307             callback.startAllExpandedTemplates();
308           }
309         });
310       }
311       else {
312         String key = template.computeTemplateKey(callback);
313         if (key != null) {
314           template.expand(key, callback);
315           callback.startAllExpandedTemplates();
316           return true;
317         }
318         // if it is simple live template invokation, we should start it using TemplateManager because template may be ambiguous
319         /*TemplateManager manager = TemplateManager.getInstance(file.getProject());
320         return manager.startTemplate(editor, TemplateSettings.getInstance().getDefaultShortcutChar());*/
321       }
322     }
323     return false;
324   }
325
326   public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
327     Editor editor = callback.getEditor();
328     PsiElement element = callback.getContext();
329     int line = editor.getCaretModel().getLogicalPosition().line;
330     int lineStart = editor.getDocument().getLineStartOffset(line);
331     int elementStart;
332     do {
333       elementStart = element != null ? element.getTextRange().getStartOffset() : 0;
334       int startOffset = elementStart > lineStart ? elementStart : lineStart;
335       String key = computeKey(editor, startOffset);
336       if (checkTemplateKey(key, callback)) {
337         return key;
338       }
339       if (element != null) {
340         element = element.getParent();
341       }
342     }
343     while (element != null && elementStart > lineStart);
344     return null;
345   }
346
347   public boolean supportsWrapping() {
348     return true;
349   }
350 }