2 * Copyright 2000-2010 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.codeInsight.template.zencoding;
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;
41 import java.util.ArrayList;
42 import java.util.Iterator;
43 import java.util.List;
47 * @author Eugene.Kudelevsky
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";
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);
66 private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
67 int eqIndex = text.indexOf('=');
69 return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
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();
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) {
90 templateKey = builder.toString();
93 if (!definedAttrs.add(ID)) {
96 attributes.add(new Pair<String, String>(ID, builder.toString()));
99 if (builder.length() <= 0) {
102 if (classAttrPosition < 0) {
103 classAttrPosition = attributes.size();
105 classes.add(builder.toString());
108 if (builder.length() > 0) {
116 else if (lastDelim != '[') {
120 Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
121 if (pair == null || !definedAttrs.add(pair.first)) {
124 attributes.add(pair);
127 builder = new StringBuilder();
133 if (classes.size() > 0) {
134 if (definedAttrs.contains(CLASS)) {
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(' ');
144 assert classAttrPosition >= 0;
145 attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
147 return new XmlTemplateToken(templateKey, attributes);
150 private static boolean isXML11ValidQName(String str) {
151 final int colon = str.indexOf(':');
152 if (colon == 0 || colon == str.length() - 1) {
156 final String prefix = str.substring(0, colon);
157 final String localPart = str.substring(colon + 1);
158 return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
160 return XML11Char.isXML11ValidNCName(str);
163 public static boolean isTrueXml(CustomTemplateCallback callback) {
164 return isTrueXml(callback.getFileType());
167 public static boolean isTrueXml(FileType type) {
168 return type == StdFileTypes.XHTML || type == StdFileTypes.JSPX || type == StdFileTypes.XML;
171 private static boolean isHtml(CustomTemplateCallback callback) {
172 FileType type = callback.getFileType();
173 return type == StdFileTypes.HTML || type == StdFileTypes.XHTML;
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)) {
186 useDefaultTag = true;
187 prefix = DEFAULT_TAG;
191 TemplateImpl template = callback.findApplicableTemplate(prefix);
192 if (template == null && !isXML11ValidQName(prefix)) {
195 final XmlTemplateToken token = parseSelectors(key);
199 if (useDefaultTag && token.getAttribute2Value().size() == 0) {
202 if (template == null) {
203 template = generateTagTemplate(token.getKey(), callback);
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) {
212 if (!XmlZenCodingInterpreter.containsAttrsVar(template) && token.getAttribute2Value().size() > 0) {
213 ApplicationManager.getApplication().runWriteAction(new Runnable() {
215 addMissingAttributes(tag, token.getAttribute2Value());
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) {
232 addAttributesBefore(tag, attr2value);
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);
245 tag.add(xmlAttribute);
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('>');
257 return new TemplateImpl("", builder.toString(), "");
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();
268 protected boolean isApplicable(@NotNull PsiElement element) {
269 if (element.getLanguage() instanceof XMLLanguage) {
270 if (PsiTreeUtil.getParentOfType(element, XmlAttributeValue.class) != null) {
273 if (PsiTreeUtil.getParentOfType(element, XmlComment.class) != null) {
276 if (!findApplicableFilter(element)) {
284 private static boolean findApplicableFilter(@NotNull PsiElement context) {
285 for (ZenCodingFilter filter : ZenCodingFilter.EP_NAME.getExtensions()) {
286 if (filter.isMyContext(context)) {
290 return new XmlZenCodingFilterImpl().isMyContext(context);
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()) {
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();
312 String key = template.computeTemplateKey(callback);
314 template.expand(key, callback);
315 callback.startAllExpandedTemplates();
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());*/
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);
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)) {
339 if (element != null) {
340 element = element.getParent();
343 while (element != null && elementStart > lineStart);
347 public boolean supportsWrapping() {