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