package com.jetbrains.lang.dart.injection;
import com.intellij.lang.Language;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.lang.injection.MultiHostInjector;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.SmartList;
import com.jetbrains.lang.dart.DartTokenTypes;
import com.jetbrains.lang.dart.psi.*;
import com.jetbrains.lang.dart.util.DartPsiImplUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class DartMultiHostInjector implements MultiHostInjector {
@Nullable private static final Language JS_REGEXP_LANG = Language.findLanguageByID("JSRegexp");
@NotNull
@Override
public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
return Collections.singletonList(DartStringLiteralExpression.class);
}
@Override
public void getLanguagesToInject(@NotNull final MultiHostRegistrar registrar, @NotNull final PsiElement element) {
if (element instanceof DartStringLiteralExpression) {
if (isRegExp((DartStringLiteralExpression)element)) {
injectRegExp(registrar, (DartStringLiteralExpression)element);
}
else {
injectHtmlIfNeeded(registrar, (DartStringLiteralExpression)element);
}
}
}
private static boolean isRegExp(@NotNull final DartStringLiteralExpression element) {
// new RegExp(r'\d+')
final PsiElement parent1 = element.getParent();
final PsiElement parentParent2 = parent1 instanceof DartArgumentList && parent1.getFirstChild() == element ? parent1.getParent() : null;
final PsiElement parent3 = parentParent2 instanceof DartArguments ? parentParent2.getParent() : null;
if (parent3 instanceof DartNewExpression) {
final DartType type = ((DartNewExpression)parent3).getType();
return type != null && "RegExp".equals(type.getText());
}
return false;
}
private static void injectRegExp(@NotNull final MultiHostRegistrar registrar, @NotNull final DartStringLiteralExpression element) {
if (JS_REGEXP_LANG == null) return; // JavaScript plugin not available
final PsiElement child = element.getFirstChild();
final IElementType elementType = child.getNode().getElementType();
if (elementType != DartTokenTypes.RAW_SINGLE_QUOTED_STRING || child.getNextSibling() != null) {
return; // inject in raw single line strings only
}
final Pair<String, TextRange> textAndRange = DartPsiImplUtil.getUnquotedDartStringAndItsRange(child.getText());
if (textAndRange.first.isEmpty()) {
return;
}
registrar.startInjecting(JS_REGEXP_LANG);
registrar.addPlace(null, null, element, textAndRange.second);
registrar.doneInjecting();
}
private static void injectHtmlIfNeeded(@NotNull final MultiHostRegistrar registrar, @NotNull final DartStringLiteralExpression element) {
final List<HtmlPlaceInfo> infos = new SmartList<>();
final StringBuilder textBuf = new StringBuilder();
PsiElement child = element.getFirstChild();
while (child != null) {
final IElementType type = child.getNode().getElementType();
if (type == DartTokenTypes.REGULAR_STRING_PART) {
textBuf.append(child.getText());
String suffix = null;
final PsiElement nextSibling = child.getNextSibling();
if (nextSibling != null && nextSibling.getNode().getElementType() != DartTokenTypes.CLOSING_QUOTE) {
suffix = "placeholder"; // string template like $foo or ${foo}
textBuf.append(suffix);
}
infos.add(new HtmlPlaceInfo(TextRange.from(child.getStartOffsetInParent(), child.getTextLength()), suffix));
}
else if (type == DartTokenTypes.RAW_SINGLE_QUOTED_STRING || type == DartTokenTypes.RAW_TRIPLE_QUOTED_STRING) {
final Pair<String, TextRange> stringAndRange = DartPsiImplUtil.getUnquotedDartStringAndItsRange(child.getText());
final String string = stringAndRange.first;
final TextRange stringRange = stringAndRange.second;
infos.add(new HtmlPlaceInfo(stringRange.shiftRight(child.getStartOffsetInParent()), null));
textBuf.append(string);
}
child = child.getNextSibling();
}
if (textBuf.length() > 0 && looksLikeHtml(textBuf.toString())) {
registrar.startInjecting(HTMLLanguage.INSTANCE);
for (HtmlPlaceInfo info : infos) {
registrar.addPlace(null, info.suffix, element, info.range);
}
registrar.doneInjecting();
}
}
private static boolean looksLikeHtml(@NotNull final String text) {
// similar to com.intellij.lang.javascript.JSInjectionController.willInjectHtml(), but strings like 'List<int>', '<foo> and <bar>' are not treated as HTML
// also, unlike JavaScript, HTML is injected in Dart only if '<' is the first non-space symbol in string
if (!text.trim().startsWith("<")) return false;
final int tagStart = text.indexOf('<');
final int length = text.length();
final boolean hasTag = tagStart >= 0
&&
(tagStart < length - 1 && (Character.isLetter(text.charAt(tagStart + 1)))
// <tag>
||
(tagStart < length - 2 && text.charAt(tagStart + 1) == '/' && Character.isLetter(text.charAt(tagStart + 2)))
// </tag>
||
(tagStart < length - 3 && text.charAt(tagStart + 1) == '!' &&
text.charAt(tagStart + 2) == '-') && text.charAt(tagStart + 3) == '-' // <!-- comment
)
&&
text.indexOf('>', tagStart) > 0;
if (hasTag) {
// now filter out cases like '<foo> and <bar>' or 'Map<int, int>'
if (Character.isLetter(text.charAt(tagStart + 1)) && !text.contains("/>") && !text.contains("</")) {
return false;
}
}
return hasTag;
}
private static class HtmlPlaceInfo {
@NotNull private final TextRange range;
@Nullable private final String suffix;
public HtmlPlaceInfo(@NotNull final TextRange range, @Nullable final String suffix) {
this.range = range;
this.suffix = suffix;
}
}
}