21274dd639f949916aa0ef386738198447b534e2
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / template / zencoding / XmlZenCodingInterpreter.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.CustomTemplateCallback;
19 import com.intellij.codeInsight.template.impl.TemplateImpl;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.fileTypes.StdFileTypes;
22 import com.intellij.openapi.util.Pair;
23 import com.intellij.psi.PsiElement;
24 import com.intellij.psi.PsiFile;
25 import com.intellij.psi.util.PsiTreeUtil;
26 import com.intellij.psi.xml.XmlTag;
27 import com.intellij.psi.xml.XmlToken;
28 import com.intellij.psi.xml.XmlTokenType;
29 import com.intellij.util.containers.HashMap;
30 import com.intellij.util.containers.HashSet;
31 import com.intellij.util.containers.IntArrayList;
32 import com.intellij.xml.util.HtmlUtil;
33 import org.jetbrains.annotations.Nullable;
34
35 import java.util.*;
36
37 /**
38  * @author Eugene.Kudelevsky
39  */
40 class XmlZenCodingInterpreter {
41   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.zencoding.XmlZenCodingInterpreter");
42   private static final String ATTRS = "ATTRS";
43
44   private final List<Token> myTokens;
45
46   private final CustomTemplateCallback myCallback;
47   private final String mySurroundedText;
48
49   private State myState;
50
51   private XmlZenCodingInterpreter(List<Token> tokens,
52                                   CustomTemplateCallback callback,
53                                   State initialState,
54                                   String surroundedText) {
55     myTokens = tokens;
56     myCallback = callback;
57     mySurroundedText = surroundedText;
58     myState = initialState;
59   }
60
61   private void finish() {
62     myCallback.gotoEndOffset();
63   }
64
65   private void gotoChild(Object templateBoundsKey) {
66     int startOfTemplate = myCallback.getStartOfTemplate(templateBoundsKey);
67     int endOfTemplate = myCallback.getEndOfTemplate(templateBoundsKey);
68     int offset = myCallback.getOffset();
69
70     PsiFile file = myCallback.parseCurrentText(StdFileTypes.XML);
71
72     PsiElement element = file.findElementAt(offset);
73     if (offset < endOfTemplate && element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
74       return;
75     }
76
77     int newOffset = -1;
78     XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, startOfTemplate, endOfTemplate, XmlTag.class);
79     if (tag != null) {
80       for (PsiElement child : tag.getChildren()) {
81         if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
82           newOffset = child.getTextOffset();
83         }
84       }
85     }
86
87     if (newOffset >= 0) {
88       if (offset < endOfTemplate) {
89         myCallback.fixEndOffset();
90       }
91       myCallback.moveToOffset(newOffset);
92     }
93   }
94
95   // returns if expanding finished
96
97   public static void interpret(List<Token> tokens,
98                                int startIndex,
99                                CustomTemplateCallback callback,
100                                State initialState,
101                                String surroundedText) {
102     XmlZenCodingInterpreter interpreter =
103       new XmlZenCodingInterpreter(tokens, callback, initialState, surroundedText);
104     interpreter.invoke(startIndex);
105   }
106
107   private void invoke(int startIndex) {
108     String filter = null;
109
110     if (myTokens.size() > 0) {
111       Token lastToken = myTokens.get(myTokens.size() - 1);
112       if (lastToken instanceof FilterToken) {
113         filter = ((FilterToken)lastToken).getSuffix();
114       }
115     }
116
117     final int n = myTokens.size();
118     TemplateToken templateToken = null;
119     int number = -1;
120     for (int i = startIndex; i < n; i++) {
121       Token token = myTokens.get(i);
122       switch (myState) {
123         case OPERATION:
124           if (templateToken != null) {
125             if (token instanceof MarkerToken || token instanceof OperationToken) {
126               final char sign = token instanceof OperationToken ? ((OperationToken)token).getSign() : ZenCodingTemplate.MARKER;
127               if (sign == '+' || (mySurroundedText == null && sign == ZenCodingTemplate.MARKER)) {
128                 final Object key = new Object();
129                 myCallback.fixStartOfTemplate(key);
130                 invokeTemplate(templateToken, myCallback, 0, filter);
131                 myState = State.WORD;
132                 if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
133                   myCallback.fixEndOffset();
134                 }
135                 if (sign == '+') {
136                   myCallback.gotoEndOfTemplate(key);
137                 }
138                 templateToken = null;
139               }
140               else if (sign == '>' || (mySurroundedText != null && sign == ZenCodingTemplate.MARKER)) {
141                 startTemplateAndGotoChild(templateToken, filter);
142                 templateToken = null;
143               }
144               else if (sign == '*') {
145                 myState = State.NUMBER;
146               }
147             }
148             else {
149               fail();
150             }
151           }
152           break;
153         case WORD:
154           if (token instanceof TemplateToken) {
155             templateToken = ((TemplateToken)token);
156             myState = State.OPERATION;
157           }
158           else {
159             fail();
160           }
161           break;
162         case NUMBER:
163           if (token instanceof NumberToken) {
164             number = ((NumberToken)token).getNumber();
165             myState = State.AFTER_NUMBER;
166           }
167           else {
168             fail();
169           }
170           break;
171         case AFTER_NUMBER:
172           if (token instanceof MarkerToken || token instanceof OperationToken) {
173             char sign = token instanceof OperationToken ? ((OperationToken)token).getSign() : ZenCodingTemplate.MARKER;
174             if (sign == '+' || (mySurroundedText == null && sign == ZenCodingTemplate.MARKER)) {
175               invokeTemplateSeveralTimes(templateToken, 0, number, filter);
176               templateToken = null;
177             }
178             else if (number > 1) {
179               invokeTemplateAndProcessTail(templateToken, 0, number, i + 1, filter);
180               return;
181             }
182             else {
183               assert number == 1;
184               startTemplateAndGotoChild(templateToken, filter);
185               templateToken = null;
186             }
187             myState = State.WORD;
188           }
189           else {
190             fail();
191           }
192           break;
193       }
194     }
195     if (mySurroundedText != null) {
196       insertText(myCallback, mySurroundedText);
197     }
198     finish();
199   }
200
201   private void startTemplateAndGotoChild(TemplateToken templateToken, String filter) {
202     final Object key = new Object();
203     myCallback.fixStartOfTemplate(key);
204     invokeTemplate(templateToken, myCallback, 0, filter);
205     myState = State.WORD;
206     gotoChild(key);
207   }
208
209   private void invokeTemplateSeveralTimes(final TemplateToken templateToken,
210                                           final int startIndex,
211                                           final int count,
212                                           String filter) {
213     final Object key = new Object();
214     myCallback.fixStartOfTemplate(key);
215     for (int i = startIndex; i < count; i++) {
216       invokeTemplate(templateToken, myCallback, i, filter);
217       myState = State.WORD;
218       if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
219         myCallback.fixEndOffset();
220       }
221       myCallback.gotoEndOfTemplate(key);
222     }
223   }
224
225   private static void insertText(CustomTemplateCallback callback, String text) {
226     int offset = callback.getOffset();
227     callback.insertString(offset, text);
228   }
229
230   private void invokeTemplateAndProcessTail(final TemplateToken templateToken,
231                                             final int startIndex,
232                                             final int count,
233                                             final int tailStart,
234                                             String filter) {
235     final Object key = new Object();
236     myCallback.fixStartOfTemplate(key);
237     for (int i = startIndex; i < count; i++) {
238       Object iterKey = new Object();
239       myCallback.fixStartOfTemplate(iterKey);
240       invokeTemplate(templateToken, myCallback, i, filter);
241       gotoChild(iterKey);
242       interpret(myTokens, tailStart, myCallback, State.WORD, mySurroundedText);
243       if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
244         myCallback.fixEndOffset();
245       }
246       myCallback.gotoEndOfTemplate(key);
247     }
248     finish();
249   }
250
251   static boolean containsAttrsVar(TemplateImpl template) {
252     for (int i = 0; i < template.getVariableCount(); i++) {
253       String varName = template.getVariableNameAt(i);
254       if (ATTRS.equals(varName)) {
255         return true;
256       }
257     }
258     return false;
259   }
260
261   private static void removeVariablesWhichHasNoSegment(TemplateImpl template) {
262     Set<String> segments = new HashSet<String>();
263     for (int i = 0; i < template.getSegmentsCount(); i++) {
264       segments.add(template.getSegmentName(i));
265     }
266     IntArrayList varsToRemove = new IntArrayList();
267     for (int i = 0; i < template.getVariableCount(); i++) {
268       String varName = template.getVariableNameAt(i);
269       if (!segments.contains(varName)) {
270         varsToRemove.add(i);
271       }
272     }
273     for (int i = 0; i < varsToRemove.size(); i++) {
274       template.removeVariable(varsToRemove.get(i));
275     }
276   }
277
278   @Nullable
279   private static Map<String, String> buildPredefinedValues(List<Pair<String, String>> attribute2value,
280                                                            int numberInIteration,
281                                                            CustomTemplateCallback callback) {
282     String attributes = buildAttributesString(attribute2value, numberInIteration, callback);
283     assert attributes != null;
284     attributes = attributes.length() > 0 ? ' ' + attributes : null;
285     Map<String, String> predefinedValues = null;
286     if (attributes != null) {
287       predefinedValues = new HashMap<String, String>();
288       predefinedValues.put(ATTRS, attributes);
289     }
290     return predefinedValues;
291   }
292
293   @Nullable
294   private static String buildAttributesString(List<Pair<String, String>> attribute2value,
295                                               int numberInIteration,
296                                               CustomTemplateCallback callback) {
297     PsiElement context = callback.getContext();
298     for (ZenCodingFilter filter : ZenCodingFilter.EP_NAME.getExtensions()) {
299       if (filter.isMyContext(context)) {
300         return filter.buildAttributesString(attribute2value, numberInIteration);
301       }
302     }
303     return new XmlZenCodingFilterImpl().buildAttributesString(attribute2value, numberInIteration);
304   }
305
306
307   private static void invokeTemplate(TemplateToken token,
308                                      final CustomTemplateCallback callback,
309                                      int numberInIteration,
310                                      String filter) {
311     if (token instanceof XmlTemplateToken && token.getTemplate() != null) {
312       XmlTemplateToken xmlTemplateToken = (XmlTemplateToken)token;
313       List<Pair<String, String>> attr2value = new ArrayList<Pair<String, String>>(xmlTemplateToken.getAttribute2Value());
314       TemplateImpl modifiedTemplate = token.getTemplate().copy();
315       XmlTag tag = xmlTemplateToken.getTag();
316       if (tag != null) {
317         for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
318           Pair<String, String> pair = iterator.next();
319           if (tag.getAttribute(pair.first) != null) {
320             tag.setAttribute(pair.first, ZenCodingUtil.getValue(pair, numberInIteration));
321             iterator.remove();
322           }
323         }
324         String s = filterXml(tag, callback, filter);
325         assert s != null;
326         if (HtmlUtil.isHtmlBlockTagL(tag.getName())) {
327           boolean newLineBefore = callback.newLineBefore();
328           boolean newLineAfter = callback.newLineAfter();
329           if (!newLineBefore || !newLineAfter) {
330             StringBuilder builder = new StringBuilder();
331             if (!newLineBefore) {
332               builder.append('\n');
333             }
334             builder.append(s);
335             if (!newLineAfter) {
336               builder.append('\n');
337             }
338             s = builder.toString();
339           }
340         }
341         modifiedTemplate.setString(s);
342         removeVariablesWhichHasNoSegment(modifiedTemplate);
343         Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration, callback);
344         callback.expandTemplate(modifiedTemplate, predefinedValues);
345       }
346     }
347     else {
348       // for CSS
349       callback.expandTemplate(token.getKey(), null);
350     }
351   }
352
353   @Nullable
354   private static String filterXml(XmlTag tag, CustomTemplateCallback callback, String filterSuffix) {
355     PsiElement context = callback.getContext();
356     for (ZenCodingFilter filter : ZenCodingFilter.EP_NAME.getExtensions()) {
357       if ((filterSuffix == null && filter.isDefaultFilter()) || (filterSuffix != null && filterSuffix.equals(filter.getSuffix()))) {
358         if (filter instanceof XmlZenCodingFilter && filter.isDefaultFilter() && filter.isMyContext(context)) {
359           return ((XmlZenCodingFilter)filter).toString(tag, context);
360         }
361       }
362     }
363     return new XmlZenCodingFilterImpl().toString(tag, context);
364   }
365
366   private static void fail() {
367     LOG.error("Input string was checked incorrectly during isApplicable() invokation");
368   }
369 }