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