package de.espend.idea.shopware.util; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.openapi.fileTypes.LanguageFileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileVisitor; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.search.FileTypeIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.FileBasedIndex; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.PhpPsiUtil; import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.smarty.SmartyFile; import com.jetbrains.smarty.SmartyFileType; import com.jetbrains.smarty.lang.SmartyTokenTypes; import com.jetbrains.smarty.lang.psi.SmartyTag; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * @author Daniel Espendiller <[email protected]> */ public class TemplateUtil { public static void collectFiles(Project project, SmartyTemplateVisitor smartyTemplateVisitor) { collectFiles(project, smartyTemplateVisitor, "tpl"); } public static void collectFiles(Project project, final SmartyTemplateVisitor smartyTemplateVisitor, String... extensions) { final List<String> exts = Arrays.asList(extensions); final List<VirtualFile> uniqueVirtualFiles = new ArrayList<>(); collectPluginTemplates(project, smartyTemplateVisitor, exts); // search for index files; think of lib and include path List<LanguageFileType> languageFileTypes = new ArrayList<>(); if(exts.contains("tpl")) { languageFileTypes.add(SmartyFileType.INSTANCE); } if(exts.contains("js")) { languageFileTypes.add(JavaScriptSupportLoader.JAVASCRIPT); } // sw5: provides parent class for themes Set<VirtualFile> themes = new HashSet<>(); for(PhpClass phpClass: PhpIndex.getInstance(project).getAllSubclasses("\\Shopware\\Components\\Theme")) { PsiDirectory parent = phpClass.getContainingFile().getParent(); if(parent != null) { themes.add(parent.getVirtualFile()); } } for(LanguageFileType fileType: languageFileTypes) { for(VirtualFile virtualFile : FileBasedIndex.getInstance().getContainingFiles(FileTypeIndex.NAME, fileType, GlobalSearchScope.allScope(project))) { if(!uniqueVirtualFiles.contains(virtualFile)) { uniqueVirtualFiles.add(virtualFile); // try to get /templates/frontend/... String path = virtualFile.toString(); int i = path.lastIndexOf("/templates/"); if(i > 0) { String frontendName = path.substring(i + "/templates/".length()); attachTemplates(virtualFile, frontendName, smartyTemplateVisitor); } else if (themes.size() > 0) { // sw5: check if file is somewhere inside a theme folder for(VirtualFile themeDir: themes) { if(VfsUtil.isAncestor(themeDir, virtualFile, false)) { String relativePath = VfsUtil.getRelativePath(virtualFile, themeDir, '/'); if(relativePath != null) { // we are too lazy prepend path name to simulate old behavior: // "Bare/frontend/campaign" attachTemplates(virtualFile, themeDir.getName() + "/" + relativePath, smartyTemplateVisitor); } } } } } } } // wooh... not a full project, so skip it // shopware is in lib only condition final VirtualFile templateDir = VfsUtil.findRelativeFile("templates", project.getBaseDir()); if(templateDir == null) { return; } // collect on project template dir VfsUtil.visitChildrenRecursively(templateDir, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile virtualFile) { if(uniqueVirtualFiles.contains(virtualFile)) { return true; } uniqueVirtualFiles.add(virtualFile); if(!isValidTemplateFile(virtualFile, exts)) { return true; } String frontendName = VfsUtil.getRelativePath(virtualFile, templateDir, '/'); if(frontendName == null) { return true; } attachTemplates(virtualFile, frontendName, smartyTemplateVisitor); return true; } }); } private static boolean attachTemplates(VirtualFile virtualFile, String frontendName, SmartyTemplateVisitor smartyTemplateVisitor) { String[] pathSplits = StringUtils.split(frontendName, "/"); if(pathSplits.length < 2 || (!"frontend".equals(pathSplits[1]) && !"backend".equals(pathSplits[1])) && !"widgets".equals(pathSplits[1])) { return true; } int i = frontendName.indexOf("/"); if(i == -1) { return true; } int n = pathSplits.length-1; String[] newArray = new String[n]; System.arraycopy(pathSplits, 1, newArray, 0, n); String fileName = StringUtils.join(newArray, "/"); smartyTemplateVisitor.visitFile(virtualFile, fileName); return false; } private static void collectPluginTemplates(Project project, final SmartyTemplateVisitor smartyTemplateVisitor, final List<String> exts) { Collection<PhpClass> phpClasses = PhpIndex.getInstance(project).getAllSubclasses("\\Shopware_Components_Plugin_Bootstrap"); for(PhpClass phpClass: phpClasses) { PsiDirectory psiDirectory = phpClass.getContainingFile().getContainingDirectory(); final VirtualFile virtualViewDir = VfsUtil.findRelativeFile("Views", psiDirectory.getVirtualFile()); if(virtualViewDir != null) { VfsUtil.visitChildrenRecursively(virtualViewDir, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { if(!isValidTemplateFile(file, exts)) { return true; } String frontendName = VfsUtil.getRelativePath(file, virtualViewDir, '/'); if(frontendName == null) { return true; } smartyTemplateVisitor.visitFile(file, frontendName); return true; } }); } } Collection<PhpClass> newPluginPhpClasses = PhpIndex.getInstance(project).getAllSubclasses("\\Shopware\\Components\\Plugin"); for(PhpClass phpClass: newPluginPhpClasses) { PsiDirectory psiDirectory = phpClass.getContainingFile().getContainingDirectory(); final VirtualFile virtualViewDir = VfsUtil.findRelativeFile("Resources/views", psiDirectory.getVirtualFile()); if(virtualViewDir != null) { VfsUtil.visitChildrenRecursively(virtualViewDir, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { if(!isValidTemplateFile(file, exts)) { return true; } String frontendName = VfsUtil.getRelativePath(file, virtualViewDir, '/'); if(frontendName == null) { return true; } smartyTemplateVisitor.visitFile(file, frontendName); return true; } }); } } } private static boolean isValidTemplateFile(VirtualFile virtualFile, List<String> extensions) { if(virtualFile.isDirectory()) { return false; } String filename = virtualFile.getName(); for(String ext: extensions) { if(filename.toLowerCase().endsWith(ext.toLowerCase())) { return true; } } return false; } public interface SmartyTemplateVisitor { void visitFile(VirtualFile virtualFile, String fileName); } public static abstract class SmartyTemplatePreventSelfVisitor implements SmartyTemplateVisitor { final VirtualFile virtualFile; public SmartyTemplatePreventSelfVisitor(VirtualFile virtualFile) { this.virtualFile = virtualFile; } public void visitFile(VirtualFile virtualFile, String fileName) { if(!this.virtualFile.equals(virtualFile)) { visitNonSelfFile(virtualFile, fileName); } } abstract public void visitNonSelfFile(VirtualFile virtualFile, String fileName); } public static boolean isExtendsTemplate(PsiFile psiFile) { for(SmartyTag smartyTag: PsiTreeUtil.getChildrenOfTypeAsList(psiFile, SmartyTag.class)) { if("extends".equals(smartyTag.getName())) { return true; } } return false; } @Nullable public static String getTemplateName(@NotNull Project project, @NotNull VirtualFile virtualFile) { return getTemplateName(project, virtualFile, "frontend", "backend", "widgets"); } /** * Find on Theme.php scope: "foo/Theme.php" => "frontend/plugins/payment/sepa" * Find on Plugin.php scope: "foo/Plugin.php" => "Resources/views/frontend/plugins/payment/sepa" */ @Nullable public static String getTemplateNameViaPath(@NotNull Project project, @NotNull VirtualFile virtualFile) { String fileNamespaceViaPath = SnippetUtil.getFileNamespaceViaPath(project, virtualFile); return fileNamespaceViaPath != null ? fileNamespaceViaPath + "." + virtualFile.getExtension() : null; } @Nullable public static String getTemplateName(@NotNull Project project, @NotNull VirtualFile virtualFile, @NotNull String... modules) { String templateNameViaPath = getTemplateNameViaPath(project, virtualFile); if (templateNameViaPath != null) { return templateNameViaPath; } // Shopware <= 5.1 "/templates/[emotion_black]/frontend" VirtualFile baseDir = project.getBaseDir(); if (baseDir == null) { return null; } String frontendName = VfsUtil.getRelativePath(virtualFile, baseDir, '/'); if(frontendName == null) { // search for possible indexed files String path = virtualFile.toString(); int i = path.lastIndexOf("/templates/"); if(i == -1) { return null; } frontendName = path.substring(i + "/templates/".length()); } // find "frontend" or any other given module inside the path for(String module: modules) { int i = frontendName.indexOf(module); if(i > 0) { return frontendName.substring(i); } } return null; } public static String cleanTemplateName(String templateName) { if(templateName.startsWith("parent:")) { templateName = templateName.substring("parent:".length()); } if(templateName.startsWith("./")) { templateName = templateName.substring("./".length()); } return templateName; } /** * {tag attribute="foobar"}{/s} */ @Nullable public static PsiElement getTagAttributeByName(@NotNull SmartyTag tag, @NotNull String attribute) { return ContainerUtil.find(YamlHelper.getChildrenFix(tag), psiElement -> SmartyPattern.getAttributeKeyPattern().accepts(psiElement) && attribute.equals(psiElement.getText()) ); } /** * {tag attribute="foobar"}{/s} */ @Nullable public static String getTagAttributeValueByName(@NotNull SmartyTag tag, @NotNull String attribute) { PsiElement psiAttribute = getTagAttributeByName(tag, attribute); if(psiAttribute == null) { return null; } PsiElement nextSibling = PhpPsiUtil.getNextSibling(psiAttribute, (Condition<PsiElement>) psiElement -> { IElementType elementType = psiElement.getNode().getElementType(); return psiElement instanceof PsiWhiteSpace || elementType == SmartyTokenTypes.EQ || elementType == SmartyTokenTypes.DOUBLE_QUOTE || elementType == SmartyTokenTypes.SINGLE_QUOTE; }); if(nextSibling == null) { return null; } String text = nextSibling.getText(); if(StringUtils.isNotBlank(text)) { return text; } return null; } /** * Find snippet namespace by tag scope and with file scope fallback * * {s namespace="foobar"} * {namespace="foobar"} */ @Nullable public static String getSnippetNamespaceByScope(@NotNull SmartyTag smartyTag) { String namespace = TemplateUtil.getTagAttributeValueByName(smartyTag, "namespace"); if(namespace != null) { return namespace; } PsiFile containingFile = smartyTag.getContainingFile(); if(containingFile instanceof SmartyFile) { return SnippetUtil.getFileNamespace((SmartyFile) containingFile); } return null; } public static String findControllerModuleFromTagContext(@NotNull PsiElement psiElement) { String module = null; PsiElement smartyTag = psiElement.getParent(); if (smartyTag instanceof SmartyTag) { String moduleValue = TemplateUtil.getTagAttributeValueByName((SmartyTag) smartyTag, "module"); if (StringUtils.isNotBlank(moduleValue)) { module = moduleValue; } } return module != null ? module : "Widgets"; } }