2 * Copyright 2000-2009 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
.xml
.util
;
18 import com
.intellij
.codeInspection
.InspectionProfile
;
19 import com
.intellij
.codeInspection
.ex
.LocalInspectionToolWrapper
;
20 import com
.intellij
.codeInspection
.htmlInspections
.HtmlUnknownAttributeInspection
;
21 import com
.intellij
.codeInspection
.htmlInspections
.HtmlUnknownTagInspection
;
22 import com
.intellij
.codeInspection
.htmlInspections
.XmlEntitiesInspection
;
23 import com
.intellij
.lang
.Language
;
24 import com
.intellij
.lang
.html
.HTMLLanguage
;
25 import com
.intellij
.lang
.xhtml
.XHTMLLanguage
;
26 import com
.intellij
.openapi
.editor
.Editor
;
27 import com
.intellij
.openapi
.util
.Ref
;
28 import com
.intellij
.openapi
.vfs
.CharsetToolkit
;
29 import com
.intellij
.profile
.codeInspection
.InspectionProjectProfileManager
;
30 import com
.intellij
.psi
.FileViewProvider
;
31 import com
.intellij
.psi
.PsiElement
;
32 import com
.intellij
.psi
.PsiFile
;
33 import com
.intellij
.psi
.html
.HtmlTag
;
34 import com
.intellij
.psi
.impl
.source
.parsing
.xml
.HtmlBuilderDriver
;
35 import com
.intellij
.psi
.impl
.source
.parsing
.xml
.XmlBuilder
;
36 import com
.intellij
.psi
.impl
.source
.xml
.XmlAttributeImpl
;
37 import com
.intellij
.psi
.templateLanguages
.TemplateLanguageFileViewProvider
;
38 import com
.intellij
.psi
.templateLanguages
.TemplateLanguageUtil
;
39 import com
.intellij
.psi
.util
.PsiTreeUtil
;
40 import com
.intellij
.psi
.xml
.XmlDocument
;
41 import com
.intellij
.psi
.xml
.XmlElement
;
42 import com
.intellij
.psi
.xml
.XmlFile
;
43 import com
.intellij
.psi
.xml
.XmlTag
;
44 import com
.intellij
.util
.ArrayUtil
;
45 import com
.intellij
.xml
.XmlAttributeDescriptor
;
46 import com
.intellij
.xml
.XmlElementDescriptor
;
47 import com
.intellij
.xml
.impl
.schema
.XmlAttributeDescriptorImpl
;
48 import com
.intellij
.xml
.impl
.schema
.XmlElementDescriptorImpl
;
49 import com
.intellij
.xml
.util
.documentation
.HtmlDescriptorsTable
;
50 import gnu
.trove
.THashSet
;
51 import org
.apache
.commons
.collections
.Bag
;
52 import org
.apache
.commons
.collections
.bag
.HashBag
;
53 import org
.jetbrains
.annotations
.NonNls
;
54 import org
.jetbrains
.annotations
.NotNull
;
55 import org
.jetbrains
.annotations
.Nullable
;
57 import java
.nio
.charset
.Charset
;
58 import java
.util
.Arrays
;
59 import java
.util
.List
;
61 import java
.util
.StringTokenizer
;
64 * @author Maxim.Mossienko
66 public class HtmlUtil
{
67 @NonNls private static final String JSFC
= "jsfc";
68 @NonNls private static final String CHARSET_PREFIX
= "charset=";
71 @NonNls private static final String
[] EMPTY_TAGS
= {
72 "base","hr","meta","link","frame","br","basefont","param","img","area","input","isindex","col"
74 private static final Set
<String
> EMPTY_TAGS_MAP
= new THashSet
<String
>();
75 @NonNls private static final String
[] OPTIONAL_END_TAGS
= {
79 "p", "li", "dd", "dt", "thead", "tfoot", "tbody", "colgroup", "tr", "th", "td", "option", "embed", "noembed"
81 private static final Set
<String
> OPTIONAL_END_TAGS_MAP
= new THashSet
<String
>();
83 @NonNls private static final String
[] BLOCK_TAGS
= { "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "dir", "menu", "pre",
84 "dl", "div", "center", "noscript", "noframes", "blockquote", "form", "isindex", "hr", "table", "fieldset", "address",
85 // nonexplicitly specified
88 "body", "object", "applet", "ins", "del", "dd", "li", "button", "th", "td", "iframe","comment","nobr"
91 // flow elements are block or inline, so they shuld not close <p> for example
92 @NonNls private static final String
[] POSSIBLY_INLINE_TAGS
= { "object", "applet", "ins", "del", "button", "nobr" };
94 private static final Set
<String
> BLOCK_TAGS_MAP
= new THashSet
<String
>();
96 @NonNls private static final String
[] INLINE_ELEMENTS_CONTAINER
= { "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "dt" };
97 private static final Set
<String
> INLINE_ELEMENTS_CONTAINER_MAP
= new THashSet
<String
>();
99 @NonNls private static final String
[] EMPTY_ATTRS
= { "nowrap", "compact", "disabled", "readonly", "selected", "multiple", "nohref", "ismap", "declare", "noshade", "checked" };
100 private static final Set
<String
> EMPTY_ATTRS_MAP
= new THashSet
<String
>();
102 private static final Set
<String
> POSSIBLY_INLINE_TAGS_MAP
= new THashSet
<String
>();
105 EMPTY_TAGS_MAP
.addAll(Arrays
.asList(EMPTY_TAGS
));
106 EMPTY_ATTRS_MAP
.addAll(Arrays
.asList(EMPTY_ATTRS
));
107 OPTIONAL_END_TAGS_MAP
.addAll(Arrays
.asList(OPTIONAL_END_TAGS
));
108 BLOCK_TAGS_MAP
.addAll(Arrays
.asList(BLOCK_TAGS
));
109 INLINE_ELEMENTS_CONTAINER_MAP
.addAll(Arrays
.asList(INLINE_ELEMENTS_CONTAINER
));
110 POSSIBLY_INLINE_TAGS_MAP
.addAll(Arrays
.asList(POSSIBLY_INLINE_TAGS
));
113 public static boolean isSingleHtmlTag(String tagName
) {
114 return EMPTY_TAGS_MAP
.contains(tagName
.toLowerCase());
117 public static boolean isSingleHtmlTagL(String tagName
) {
118 return EMPTY_TAGS_MAP
.contains(tagName
);
121 public static boolean isOptionalEndForHtmlTag(String tagName
) {
122 return OPTIONAL_END_TAGS_MAP
.contains(tagName
.toLowerCase());
125 public static boolean isOptionalEndForHtmlTagL(String tagName
) {
126 return OPTIONAL_END_TAGS_MAP
.contains(tagName
);
129 public static boolean isSingleHtmlAttribute(String attrName
) {
130 return EMPTY_ATTRS_MAP
.contains(attrName
.toLowerCase());
133 public static boolean isHtmlBlockTag(String tagName
) {
134 return BLOCK_TAGS_MAP
.contains(tagName
.toLowerCase());
137 public static boolean isPossiblyInlineTag(String tagName
) {
138 return POSSIBLY_INLINE_TAGS_MAP
.contains(tagName
);
141 public static boolean isHtmlBlockTagL(String tagName
) {
142 return BLOCK_TAGS_MAP
.contains(tagName
);
145 public static boolean isInlineTagContainer(String tagName
) {
146 return INLINE_ELEMENTS_CONTAINER_MAP
.contains(tagName
.toLowerCase());
149 public static boolean isInlineTagContainerL(String tagName
) {
150 return INLINE_ELEMENTS_CONTAINER_MAP
.contains(tagName
);
153 public static void addHtmlSpecificCompletions(final XmlElementDescriptor descriptor
,
154 final XmlTag element
,
155 final List
<XmlElementDescriptor
> variants
) {
156 // add html block completions for tags with optional ends!
157 String name
= descriptor
.getName(element
);
159 if (name
!= null && isOptionalEndForHtmlTag(name
)) {
160 PsiElement parent
= element
.getParent();
163 // we need grand parent since completion already uses parent's descriptor
164 parent
= parent
.getParent();
167 if (parent
instanceof HtmlTag
) {
168 final XmlElementDescriptor parentDescriptor
= ((HtmlTag
)parent
).getDescriptor();
170 if (parentDescriptor
!=descriptor
&& parentDescriptor
!=null) {
171 for (final XmlElementDescriptor elementsDescriptor
: parentDescriptor
.getElementsDescriptors((XmlTag
)parent
)) {
172 if (isHtmlBlockTag(elementsDescriptor
.getName())) {
173 variants
.add(elementsDescriptor
);
182 public static XmlDocument
getRealXmlDocument(@Nullable XmlDocument doc
) {
183 if (doc
== null) return null;
184 final PsiFile containingFile
= doc
.getContainingFile();
186 final PsiFile templateFile
= TemplateLanguageUtil
.getTemplateFile(containingFile
);
187 if (templateFile
instanceof XmlFile
) {
188 return ((XmlFile
)templateFile
).getDocument();
193 public static String
[] getHtmlTagNames() {
194 return HtmlDescriptorsTable
.getHtmlTagNames();
197 public static XmlAttributeDescriptor
[] getCustomAttributeDescriptors(XmlElement context
) {
198 String entitiesString
= getEntitiesString(context
, XmlEntitiesInspection
.UNKNOWN_ATTRIBUTE
);
199 if (entitiesString
== null) return XmlAttributeDescriptor
.EMPTY
;
201 StringTokenizer tokenizer
= new StringTokenizer(entitiesString
,",");
202 XmlAttributeDescriptor
[] descriptors
= new XmlAttributeDescriptor
[tokenizer
.countTokens()];
205 while(tokenizer
.hasMoreElements()) {
206 final String customName
= tokenizer
.nextToken();
207 if (customName
.length() == 0) continue;
209 descriptors
[index
++] = new XmlAttributeDescriptorImpl() {
210 public String
getName(PsiElement context
) {
214 public String
getName() {
223 public static XmlElementDescriptor
[] getCustomTagDescriptors(XmlElement context
) {
224 String entitiesString
= getEntitiesString(context
, XmlEntitiesInspection
.UNKNOWN_TAG
);
225 if (entitiesString
== null) return XmlElementDescriptor
.EMPTY_ARRAY
;
227 StringTokenizer tokenizer
= new StringTokenizer(entitiesString
, ",");
228 XmlElementDescriptor
[] descriptors
= new XmlElementDescriptor
[tokenizer
.countTokens()];
231 while(tokenizer
.hasMoreElements()) {
232 final String tagName
= tokenizer
.nextToken();
233 if (tagName
.length() == 0) continue;
235 descriptors
[index
++] = new XmlElementDescriptorImpl(context
instanceof XmlTag ?
(XmlTag
)context
:null) {
236 public String
getName(PsiElement context
) {
240 public String
getDefaultName() {
244 public boolean allowElementsFromNamespace(final String namespace
, final XmlTag context
) {
254 public static String
getEntitiesString(XmlElement context
, int type
) {
255 if (context
== null) return null;
256 PsiFile containingFile
= context
.getContainingFile().getOriginalFile();
258 final InspectionProfile profile
= InspectionProjectProfileManager
.getInstance(context
.getProject()).getInspectionProfile();
261 case XmlEntitiesInspection
.UNKNOWN_TAG
:
262 LocalInspectionToolWrapper wrapper
= (LocalInspectionToolWrapper
) profile
.getInspectionTool(HtmlUnknownTagInspection
.TAG_SHORT_NAME
,
264 HtmlUnknownTagInspection unknownTagInspection
= wrapper
!= null ?
(HtmlUnknownTagInspection
) wrapper
.getTool() : null;
265 if (unknownTagInspection
!= null) {
266 return unknownTagInspection
.getAdditionalEntries();
269 case XmlEntitiesInspection
.UNKNOWN_ATTRIBUTE
:
270 LocalInspectionToolWrapper wrapper1
= (LocalInspectionToolWrapper
) profile
.getInspectionTool(HtmlUnknownAttributeInspection
.ATTRIBUTE_SHORT_NAME
,
272 HtmlUnknownAttributeInspection unknownAttributeInspection
= wrapper1
!= null ?
(HtmlUnknownAttributeInspection
) wrapper1
.getTool() : null;
273 if (unknownAttributeInspection
!= null) {
274 return unknownAttributeInspection
.getAdditionalEntries();
282 public static XmlAttributeDescriptor
[] appendHtmlSpecificAttributeCompletions(final XmlTag declarationTag
,
283 XmlAttributeDescriptor
[] descriptors
,
284 final XmlAttributeImpl context
) {
285 if (declarationTag
instanceof HtmlTag
) {
286 descriptors
= ArrayUtil
.mergeArrays(
288 getCustomAttributeDescriptors(context
),
289 XmlAttributeDescriptor
.class
294 if (declarationTag
.getPrefixByNamespace(XmlUtil
.JSF_HTML_URI
) != null &&
295 declarationTag
.getNSDescriptor(XmlUtil
.XHTML_URI
, true) != null &&
296 !XmlUtil
.JSP_URI
.equals(declarationTag
.getNamespace())) {
298 descriptors
= ArrayUtil
.append(
300 new XmlAttributeDescriptorImpl() {
301 public String
getName(PsiElement context
) {
305 public String
getName() {
314 private static class TerminateException
extends RuntimeException
{
315 private static final TerminateException INSTANCE
= new TerminateException();
317 public static Charset
detectCharsetFromMetaHttpEquiv(@NotNull String content
) {
318 final Ref
<String
> charsetNameRef
= new Ref
<String
>();
320 new HtmlBuilderDriver(content
).build(new XmlBuilder() {
321 @NonNls final Bag inTag
= new HashBag();
322 boolean metHttpEquiv
= false;
324 public void doctype(@Nullable final CharSequence publicId
, @Nullable final CharSequence systemId
, final int startOffset
, final int endOffset
) {
327 public ProcessingOrder
startTag(final CharSequence localName
, final String namespace
, final int startoffset
, final int endoffset
,
328 final int headerEndOffset
) {
329 @NonNls String name
= localName
.toString().toLowerCase();
331 if (!inTag
.contains("head") && !"html".equals(name
)) terminate();
332 return ProcessingOrder
.TAGS_AND_ATTRIBUTES
;
335 private void terminate() {
336 throw TerminateException
.INSTANCE
;
339 public void endTag(final CharSequence localName
, final String namespace
, final int startoffset
, final int endoffset
) {
340 @NonNls final String name
= localName
.toString().toLowerCase();
341 if ("meta".equals(name
) && metHttpEquiv
&& contentAttributeValue
!= null) {
342 int start
= contentAttributeValue
.indexOf(CHARSET_PREFIX
);
343 if (start
== -1) return;
344 start
+= CHARSET_PREFIX
.length();
345 int end
= contentAttributeValue
.indexOf(';', start
);
346 if (end
== -1) end
= contentAttributeValue
.length();
347 String charsetName
= contentAttributeValue
.substring(start
, end
);
348 charsetNameRef
.set(charsetName
);
351 if ("head".equals(name
)) {
355 metHttpEquiv
= false;
356 contentAttributeValue
= null;
359 private String contentAttributeValue
;
360 public void attribute(final CharSequence localName
, final CharSequence v
, final int startoffset
, final int endoffset
) {
361 @NonNls final String name
= localName
.toString().toLowerCase();
362 if (inTag
.contains("meta")) {
363 @NonNls String value
= v
.toString().toLowerCase();
364 if (name
.equals("http-equiv")) {
365 metHttpEquiv
|= value
.equals("content-type");
367 if (name
.equals("content")) {
368 contentAttributeValue
= value
;
373 public void textElement(final CharSequence display
, final CharSequence physical
, final int startoffset
, final int endoffset
) {
376 public void entityRef(final CharSequence ref
, final int startOffset
, final int endOffset
) {
379 public void error(String message
, int startOffset
, int endOffset
) {
383 catch (TerminateException e
) {
386 catch (Exception e
) {
387 // some weird things can happen, like unbalanaced tree
390 String name
= charsetNameRef
.get();
391 return CharsetToolkit
.forName(name
);
394 public static boolean isTagWithoutAttributes(@NonNls String tagName
) {
395 return tagName
!= null && "br".equalsIgnoreCase(tagName
);
398 public static boolean hasHtml(PsiFile file
) {
399 if (isHtmlFile(file
)) return true;
400 if (file
.getViewProvider() instanceof TemplateLanguageFileViewProvider
) return true;
404 private static boolean isHtmlFile(final PsiFile file
) {
405 final Language language
= file
.getLanguage();
406 if (language
== HTMLLanguage
.INSTANCE
|| language
== XHTMLLanguage
.INSTANCE
) {
412 public static boolean isHtmlTagContainingFile(PsiElement element
) {
413 if (element
== null) {
416 final PsiFile containingFile
= element
.getContainingFile();
417 if (containingFile
!= null) {
418 final XmlTag tag
= PsiTreeUtil
.getParentOfType(element
, XmlTag
.class, false);
419 if (tag
instanceof HtmlTag
) {
423 final FileViewProvider provider
= containingFile
.getViewProvider();
425 if (provider
instanceof TemplateLanguageFileViewProvider
) {
426 language
= ((TemplateLanguageFileViewProvider
)provider
).getTemplateDataLanguage();
429 language
= provider
.getBaseLanguage();
432 return language
== XHTMLLanguage
.INSTANCE
;
438 public static boolean isHtmlTagContainingFile(final Editor editor
, final PsiFile file
) {
439 if (editor
== null || file
== null || !(file
instanceof XmlFile
)) {
442 final int offset
= editor
.getCaretModel().getOffset();
443 final PsiElement element
= file
.findElementAt(offset
);
444 return isHtmlTagContainingFile(element
);