blob: 4fa16177bf868c4c9b9841fee4d108368d81ed4e [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.xml.util;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.htmlInspections.XmlEntitiesInspection;
import com.intellij.ide.highlighter.HtmlFileType;
import com.intellij.ide.highlighter.XHtmlFileType;
import com.intellij.javaee.ExternalResourceManagerEx;
import com.intellij.lang.Language;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.lang.xhtml.XHTMLLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.FileViewProvider;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.impl.source.html.HtmlDocumentImpl;
import com.intellij.psi.impl.source.parsing.xml.HtmlBuilderDriver;
import com.intellij.psi.impl.source.parsing.xml.XmlBuilder;
import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.Html5SchemaProvider;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import com.intellij.xml.XmlNSDescriptor;
import com.intellij.xml.impl.schema.XmlAttributeDescriptorImpl;
import com.intellij.xml.impl.schema.XmlElementDescriptorImpl;
import com.intellij.xml.util.documentation.HtmlDescriptorsTable;
import com.intellij.xml.util.documentation.MimeTypeDictionary;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
/**
* @author Maxim.Mossienko
*/
public class HtmlUtil {
private static final Logger LOG = Logger.getInstance("#com.intellij.xml.util.HtmlUtil");
@NonNls private static final String JSFC = "jsfc";
@NonNls private static final String CHARSET = "charset";
@NonNls private static final String CHARSET_PREFIX = CHARSET+"=";
@NonNls private static final String HTML5_DATA_ATTR_PREFIX = "data-";
public static final String SCRIPT_TAG_NAME = "script";
public static final String[] CONTENT_TYPES = ArrayUtil.toStringArray(MimeTypeDictionary.getContentTypes());
@NonNls public static final String MATH_ML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
@NonNls public static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
public static final String[] RFC2616_HEADERS = new String[]{"Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language",
"Accept-Ranges", "Age", "Allow", "Authorization", "Cache-Control", "Connection", "Content-Encoding", "Content-Language",
"Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Date", "ETag", "Expect", "Expires", "From",
"Host", "If-Match", "If-Modified-Since", "If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location",
"Max-Forwards", "Pragma", "Proxy-Authenticate", "Proxy-Authorization", "Range", "Referer", "Refresh", "Retry-After", "Server", "TE",
"Trailer", "Transfer-Encoding", "Upgrade", "User-Agent", "Vary", "Via", "Warning", "WWW-Authenticate"};
private HtmlUtil() {
}
private static final Set<String> EMPTY_TAGS_MAP = new THashSet<String>();
@NonNls private static final String[] OPTIONAL_END_TAGS = {
//"html",
"head",
//"body",
"p", "li", "dd", "dt", "thead", "tfoot", "tbody", "colgroup", "tr", "th", "td", "option", "embed", "noembed"
};
private static final Set<String> OPTIONAL_END_TAGS_MAP = new THashSet<String>();
@NonNls private static final String[] BLOCK_TAGS = {"p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "dir", "menu", "pre",
"dl", "div", "center", "noscript", "noframes", "blockquote", "form", "isindex", "hr", "table", "fieldset", "address",
// nonexplicitly specified
"map",
// flow elements
"body", "object", "applet", "ins", "del", "dd", "li", "button", "th", "td", "iframe", "comment", "nobr"
};
// flow elements are block or inline, so they should not close <p> for example
@NonNls private static final String[] POSSIBLY_INLINE_TAGS =
{"a", "abbr", "acronym", "applet", "b", "basefont", "bdo", "big", "br", "button",
"cite", "code", "del", "dfn", "em", "font", "i", "iframe", "img", "input", "ins",
"kbd", "label", "map", "object", "q", "s", "samp", "select", "small", "span", "strike",
"strong", "sub", "sup", "textarea", "tt", "u", "var"};
private static final Set<String> BLOCK_TAGS_MAP = new THashSet<String>();
@NonNls private static final String[] INLINE_ELEMENTS_CONTAINER = {"p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "dt"};
private static final Set<String> INLINE_ELEMENTS_CONTAINER_MAP = new THashSet<String>();
@NonNls private static final String[] EMPTY_ATTRS =
{"nowrap", "compact", "disabled", "readonly", "selected", "multiple", "nohref", "ismap", "declare", "noshade", "checked"};
private static final Set<String> EMPTY_ATTRS_MAP = new THashSet<String>();
private static final Set<String> POSSIBLY_INLINE_TAGS_MAP = new THashSet<String>();
@NonNls private static final String[] HTML5_TAGS = {
"article", "aside", "audio", "canvas", "command", "datalist", "details", "embed", "figcaption", "figure", "footer", "header",
"keygen", "mark", "meter", "nav", "output", "progress", "rp", "rt", "ruby", "section", "source", "summary", "time", "video", "wbr",
"main"
};
private static final Set<String> HTML5_TAGS_SET = new THashSet<String>();
private static final Map<String, Set<String>> AUTO_CLOSE_BY_MAP = new THashMap<String, Set<String>>();
static {
for (HTMLControls.Control control : HTMLControls.getControls()) {
final String tagName = control.name.toLowerCase();
if (control.endTag == HTMLControls.TagState.FORBIDDEN) EMPTY_TAGS_MAP.add(tagName);
AUTO_CLOSE_BY_MAP.put(tagName, new THashSet<String>(control.autoClosedBy));
}
ContainerUtil.addAll(EMPTY_ATTRS_MAP, EMPTY_ATTRS);
ContainerUtil.addAll(OPTIONAL_END_TAGS_MAP, OPTIONAL_END_TAGS);
ContainerUtil.addAll(BLOCK_TAGS_MAP, BLOCK_TAGS);
ContainerUtil.addAll(INLINE_ELEMENTS_CONTAINER_MAP, INLINE_ELEMENTS_CONTAINER);
ContainerUtil.addAll(POSSIBLY_INLINE_TAGS_MAP, POSSIBLY_INLINE_TAGS);
ContainerUtil.addAll(HTML5_TAGS_SET, HTML5_TAGS);
}
public static boolean isSingleHtmlTag(String tagName) {
return EMPTY_TAGS_MAP.contains(tagName.toLowerCase());
}
public static boolean isSingleHtmlTagL(String tagName) {
return EMPTY_TAGS_MAP.contains(tagName);
}
public static boolean isOptionalEndForHtmlTag(String tagName) {
return OPTIONAL_END_TAGS_MAP.contains(tagName.toLowerCase());
}
public static boolean isOptionalEndForHtmlTagL(String tagName) {
return OPTIONAL_END_TAGS_MAP.contains(tagName);
}
public static boolean canTerminate(final String childTagName, final String tagName) {
final Set<String> closingTags = AUTO_CLOSE_BY_MAP.get(tagName);
return closingTags != null && closingTags.contains(childTagName);
}
public static boolean isSingleHtmlAttribute(String attrName) {
return EMPTY_ATTRS_MAP.contains(attrName.toLowerCase());
}
public static boolean isHtmlBlockTag(String tagName) {
return BLOCK_TAGS_MAP.contains(tagName.toLowerCase());
}
public static boolean isPossiblyInlineTag(String tagName) {
return POSSIBLY_INLINE_TAGS_MAP.contains(tagName);
}
public static boolean isHtmlBlockTagL(String tagName) {
return BLOCK_TAGS_MAP.contains(tagName);
}
public static boolean isInlineTagContainer(String tagName) {
return INLINE_ELEMENTS_CONTAINER_MAP.contains(tagName.toLowerCase());
}
public static boolean isInlineTagContainerL(String tagName) {
return INLINE_ELEMENTS_CONTAINER_MAP.contains(tagName);
}
public static void addHtmlSpecificCompletions(final XmlElementDescriptor descriptor,
final XmlTag element,
final List<XmlElementDescriptor> variants) {
// add html block completions for tags with optional ends!
String name = descriptor.getName(element);
if (name != null && isOptionalEndForHtmlTag(name)) {
PsiElement parent = element.getParent();
if (parent != null) {
// we need grand parent since completion already uses parent's descriptor
parent = parent.getParent();
}
if (parent instanceof HtmlTag) {
final XmlElementDescriptor parentDescriptor = ((HtmlTag)parent).getDescriptor();
if (parentDescriptor != descriptor && parentDescriptor != null) {
for (final XmlElementDescriptor elementsDescriptor : parentDescriptor.getElementsDescriptors((XmlTag)parent)) {
if (isHtmlBlockTag(elementsDescriptor.getName())) {
variants.add(elementsDescriptor);
}
}
}
} else if (parent instanceof HtmlDocumentImpl) {
final XmlNSDescriptor nsDescriptor = descriptor.getNSDescriptor();
for (XmlElementDescriptor elementDescriptor : nsDescriptor.getRootElementsDescriptors((XmlDocument)parent)) {
if (isHtmlBlockTag(elementDescriptor.getName()) && !variants.contains(elementDescriptor)) {
variants.add(elementDescriptor);
}
}
}
}
}
@Nullable
public static XmlDocument getRealXmlDocument(@Nullable XmlDocument doc) {
return HtmlPsiUtil.getRealXmlDocument(doc);
}
public static String[] getHtmlTagNames() {
return HtmlDescriptorsTable.getHtmlTagNames();
}
public static XmlAttributeDescriptor[] getCustomAttributeDescriptors(XmlElement context) {
String entitiesString = getEntitiesString(context, XmlEntitiesInspection.ATTRIBUTE_SHORT_NAME);
if (entitiesString == null) return XmlAttributeDescriptor.EMPTY;
StringTokenizer tokenizer = new StringTokenizer(entitiesString, ",");
XmlAttributeDescriptor[] descriptors = new XmlAttributeDescriptor[tokenizer.countTokens()];
int index = 0;
while (tokenizer.hasMoreElements()) {
final String customName = tokenizer.nextToken();
if (customName.length() == 0) continue;
descriptors[index++] = new XmlAttributeDescriptorImpl() {
@Override
public String getName(PsiElement context) {
return customName;
}
@Override
public String getName() {
return customName;
}
};
}
return descriptors;
}
public static XmlElementDescriptor[] getCustomTagDescriptors(@Nullable PsiElement context) {
String entitiesString = getEntitiesString(context, XmlEntitiesInspection.TAG_SHORT_NAME);
if (entitiesString == null) return XmlElementDescriptor.EMPTY_ARRAY;
StringTokenizer tokenizer = new StringTokenizer(entitiesString, ",");
XmlElementDescriptor[] descriptors = new XmlElementDescriptor[tokenizer.countTokens()];
int index = 0;
while (tokenizer.hasMoreElements()) {
final String tagName = tokenizer.nextToken();
if (tagName.length() == 0) continue;
descriptors[index++] = new XmlElementDescriptorImpl(context instanceof XmlTag ? (XmlTag)context : null) {
@Override
public String getName(PsiElement context) {
return tagName;
}
@Override
public String getDefaultName() {
return tagName;
}
@Override
public boolean allowElementsFromNamespace(final String namespace, final XmlTag context) {
return true;
}
};
}
return descriptors;
}
@Nullable
public static String getEntitiesString(@Nullable PsiElement context, @NotNull String inspectionName) {
if (context == null) return null;
PsiFile containingFile = context.getContainingFile().getOriginalFile();
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(context.getProject()).getInspectionProfile();
XmlEntitiesInspection inspection = (XmlEntitiesInspection)profile.getUnwrappedTool(inspectionName, containingFile);
if (inspection != null) {
return inspection.getAdditionalEntries();
}
return null;
}
public static XmlAttributeDescriptor[] appendHtmlSpecificAttributeCompletions(final XmlTag declarationTag,
XmlAttributeDescriptor[] descriptors,
final XmlAttribute context) {
if (declarationTag instanceof HtmlTag) {
descriptors = ArrayUtil.mergeArrays(
descriptors,
getCustomAttributeDescriptors(context)
);
return descriptors;
}
boolean isJsfHtmlNamespace = false;
for (String jsfHtmlUri : XmlUtil.JSF_HTML_URIS) {
if (declarationTag.getPrefixByNamespace(jsfHtmlUri) != null) {
isJsfHtmlNamespace = true;
break;
}
}
if (isJsfHtmlNamespace && declarationTag.getNSDescriptor(XmlUtil.XHTML_URI, true) != null &&
!XmlUtil.JSP_URI.equals(declarationTag.getNamespace())) {
descriptors = ArrayUtil.append(
descriptors,
new XmlAttributeDescriptorImpl() {
@Override
public String getName(PsiElement context) {
return JSFC;
}
@Override
public String getName() {
return JSFC;
}
},
XmlAttributeDescriptor.class
);
}
return descriptors;
}
public static boolean isHtml5Document(XmlDocument doc) {
if (doc == null) {
return false;
}
XmlProlog prolog = doc.getProlog();
XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null;
if (!isHtmlTagContainingFile(doc)) {
return false;
}
final PsiFile htmlFile = doc.getContainingFile();
final String htmlFileFullName;
if (htmlFile != null) {
final VirtualFile vFile = htmlFile.getVirtualFile();
if (vFile != null) {
htmlFileFullName = vFile.getPath();
}
else {
htmlFileFullName = htmlFile.getName();
}
}
else {
htmlFileFullName = "unknown";
}
if (doctype == null) {
LOG.debug("DOCTYPE for " + htmlFileFullName + " is null");
return Html5SchemaProvider.getHtml5SchemaLocation()
.equals(ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(doc.getProject()));
}
final boolean html5Doctype = isHtml5Doctype(doctype);
final String doctypeDescription = "text: " + doctype.getText() +
", dtdUri: " + doctype.getDtdUri() +
", publicId: " + doctype.getPublicId() +
", markupDecl: " + doctype.getMarkupDecl();
LOG.debug("DOCTYPE for " + htmlFileFullName + "; " + doctypeDescription + "; HTML5: " + html5Doctype);
return html5Doctype;
}
public static boolean isHtml5Doctype(XmlDoctype doctype) {
return doctype.getDtdUri() == null && doctype.getPublicId() == null && doctype.getMarkupDecl() == null;
}
public static boolean isHtml5Context(XmlElement context) {
XmlDocument doc = PsiTreeUtil.getParentOfType(context, XmlDocument.class);
return isHtml5Document(doc);
}
public static boolean isHtmlTag(@NotNull XmlTag tag) {
if (tag.getLanguage() != HTMLLanguage.INSTANCE) return false;
XmlDocument doc = PsiTreeUtil.getParentOfType(tag, XmlDocument.class);
String doctype = null;
if (doc != null) {
doctype = XmlUtil.getDtdUri(doc);
}
doctype = doctype == null ? ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(tag.getProject()) : doctype;
return XmlUtil.XHTML4_SCHEMA_LOCATION.equals(doctype) ||
!StringUtil.containsIgnoreCase(doctype, "xhtml");
}
public static boolean hasNonHtml5Doctype(XmlElement context) {
XmlDocument doc = PsiTreeUtil.getParentOfType(context, XmlDocument.class);
if (doc == null) {
return false;
}
XmlProlog prolog = doc.getProlog();
XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null;
return doctype != null && !isHtml5Doctype(doctype);
}
public static boolean isHtml5Tag(String tagName) {
return HTML5_TAGS_SET.contains(tagName);
}
public static boolean isCustomHtml5Attribute(String attributeName) {
return attributeName.startsWith(HTML5_DATA_ATTR_PREFIX);
}
@Nullable
public static String getHrefBase(XmlFile file) {
final XmlTag root = file.getRootTag();
final XmlTag head = root != null ? root.findFirstSubTag("head") : null;
final XmlTag base = head != null ? head.findFirstSubTag("base") : null;
return base != null ? base.getAttributeValue("href") : null;
}
public static boolean isOwnHtmlAttribute(XmlAttributeDescriptor descriptor) {
// common html attributes are defined mostly in common.rnc, core-scripting.rnc, etc
// while own tag attributes are defined in meta.rnc
final PsiElement declaration = descriptor.getDeclaration();
final PsiFile file = declaration != null ? declaration.getContainingFile() : null;
final String name = file != null ? file.getName() : null;
return "meta.rnc".equals(name);
}
private static class TerminateException extends RuntimeException {
private static final TerminateException INSTANCE = new TerminateException();
}
public static Charset detectCharsetFromMetaTag(@NotNull String content) {
// check for <meta http-equiv="charset=CharsetName" > or <meta charset="CharsetName"> and return Charset
// because we will lightly parse and explicit charset isn't used very often do quick check for applicability
int charPrefix = content.indexOf(CHARSET);
do {
if (charPrefix == -1) return null;
int charsetPrefixEnd = charPrefix + CHARSET.length();
while (charsetPrefixEnd < content.length() && Character.isWhitespace(content.charAt(charsetPrefixEnd))) ++charsetPrefixEnd;
if (charsetPrefixEnd < content.length() && content.charAt(charsetPrefixEnd) == '=') break;
charPrefix = content.indexOf(CHARSET, charsetPrefixEnd);
} while(true);
final Ref<String> charsetNameRef = new Ref<String>();
try {
new HtmlBuilderDriver(content).build(new XmlBuilder() {
@NonNls final Set<String> inTag = new THashSet<String>();
boolean metHttpEquiv = false;
boolean metHttml5Charset = false;
@Override
public void doctype(@Nullable final CharSequence publicId,
@Nullable final CharSequence systemId,
final int startOffset,
final int endOffset) {
}
@Override
public ProcessingOrder startTag(final CharSequence localName, final String namespace, final int startoffset, final int endoffset,
final int headerEndOffset) {
@NonNls String name = localName.toString().toLowerCase();
inTag.add(name);
if (!inTag.contains("head") && !"html".equals(name)) terminate();
return ProcessingOrder.TAGS_AND_ATTRIBUTES;
}
private void terminate() {
throw TerminateException.INSTANCE;
}
@Override
public void endTag(final CharSequence localName, final String namespace, final int startoffset, final int endoffset) {
@NonNls final String name = localName.toString().toLowerCase();
if ("meta".equals(name) && (metHttpEquiv || metHttml5Charset) && contentAttributeValue != null) {
String charsetName;
if (metHttpEquiv) {
int start = contentAttributeValue.indexOf(CHARSET_PREFIX);
if (start == -1) return;
start += CHARSET_PREFIX.length();
int end = contentAttributeValue.indexOf(';', start);
if (end == -1) end = contentAttributeValue.length();
charsetName = contentAttributeValue.substring(start, end);
} else /*if (metHttml5Charset) */ {
charsetName = StringUtil.stripQuotesAroundValue(contentAttributeValue);
}
charsetNameRef.set(charsetName);
terminate();
}
if ("head".equals(name)) {
terminate();
}
inTag.remove(name);
metHttpEquiv = false;
metHttml5Charset = false;
contentAttributeValue = null;
}
private String contentAttributeValue;
@Override
public void attribute(final CharSequence localName, final CharSequence v, final int startoffset, final int endoffset) {
@NonNls final String name = localName.toString().toLowerCase();
if (inTag.contains("meta")) {
@NonNls String value = v.toString().toLowerCase();
if (name.equals("http-equiv")) {
metHttpEquiv |= value.equals("content-type");
} else if (name.equals(CHARSET)) {
metHttml5Charset = true;
contentAttributeValue = value;
}
if (name.equals("content")) {
contentAttributeValue = value;
}
}
}
@Override
public void textElement(final CharSequence display, final CharSequence physical, final int startoffset, final int endoffset) {
}
@Override
public void entityRef(final CharSequence ref, final int startOffset, final int endOffset) {
}
@Override
public void error(String message, int startOffset, int endOffset) {
}
});
}
catch (TerminateException ignored) {
//ignore
}
catch (Exception ignored) {
// some weird things can happen, like unbalanaced tree
}
String name = charsetNameRef.get();
return CharsetToolkit.forName(name);
}
public static boolean isTagWithoutAttributes(@NonNls String tagName) {
return tagName != null && "br".equalsIgnoreCase(tagName);
}
public static boolean hasHtml(PsiFile file) {
return isHtmlFile(file) || file.getViewProvider() instanceof TemplateLanguageFileViewProvider;
}
public static boolean hasHtmlPrefix(@NotNull String url) {
return url.startsWith("http://") ||
url.startsWith("https://") ||
url.startsWith("//") || //Protocol-relative URL
url.startsWith("ftp://");
}
public static boolean isHtmlFile(@NotNull PsiElement element) {
Language language = element.getLanguage();
return language == HTMLLanguage.INSTANCE || language == XHTMLLanguage.INSTANCE;
}
public static boolean isHtmlFile(@NotNull VirtualFile file) {
FileType fileType = file.getFileType();
return fileType == HtmlFileType.INSTANCE || fileType == XHtmlFileType.INSTANCE;
}
public static boolean isHtmlTagContainingFile(PsiElement element) {
if (element == null) {
return false;
}
final PsiFile containingFile = element.getContainingFile();
if (containingFile != null) {
final XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class, false);
if (tag instanceof HtmlTag) {
return true;
}
final XmlDocument document = PsiTreeUtil.getParentOfType(element, XmlDocument.class, false);
if (document instanceof HtmlDocumentImpl) {
return true;
}
final FileViewProvider provider = containingFile.getViewProvider();
Language language;
if (provider instanceof TemplateLanguageFileViewProvider) {
language = ((TemplateLanguageFileViewProvider)provider).getTemplateDataLanguage();
}
else {
language = provider.getBaseLanguage();
}
return language == XHTMLLanguage.INSTANCE;
}
return false;
}
public static boolean isScriptTag(@Nullable XmlTag tag) {
return tag != null && tag.getLocalName().equalsIgnoreCase(SCRIPT_TAG_NAME);
}
}