/*
* Copyright 2010 Ronnie Kolehmainen
*
* 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.github.cssxfire;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.PsiElement;
import com.intellij.psi.css.*;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.search.TextOccurenceProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* Created by IntelliJ IDEA.
* User: Ronnie
*/
public class CssSelectorSearchProcessor implements TextOccurenceProcessor {
private static final Logger LOG = Logger.getInstance(CssSelectorSearchProcessor.class.getName());
private static final String DUMMY = "";
private final List<CssElement> selectors = new ArrayList<CssElement>();
@NotNull
private String selector;
@NotNull
private String word;
public CssSelectorSearchProcessor(@NotNull String selector) {
this.selector = StringUtils.normalizeWhitespace(selector);
this.word = StringUtils.extractSearchWord(this.selector);
}
/**
* Get the word to use when using {@link com.intellij.psi.search.PsiSearchHelper} to process elements with word
*
* @return the word to use in search
*/
@NotNull
public String getSearchWord() {
return word;
}
/**
* Get the entire selector string, with whitespace normalized
*
* @return the selector string used for final match
*/
@NotNull
public String getSelector() {
return selector;
}
public boolean execute(PsiElement psiElement, int i) {
if (psiElement instanceof CssSelector || psiElement instanceof CssSelectorList) {
CssElement cssSelector = (CssElement) psiElement;
if ((!(cssSelector.getParent() instanceof CssSelectorList)) && canBeReference(cssSelector)) {
selectors.add(cssSelector);
}
}
return true;
}
/**
* Checks (stringwise) if the given css element could reference the selector to search for. The check is
* done by expanding the rule of the given css selector by checking all of its parent elements within the
* same file. This allows for nested rules (Less/Sass).
*
* @param cssSelector the candidate element
* @return true if and only if the rule of the given element matches this selector
*/
private boolean canBeReference(@NotNull CssElement cssSelector) {
final List<List<String>> selectorPaths = createSelectorParts(selector);
boolean complete = CssUtils.processParents(cssSelector, new PsiElementProcessor<PsiElement>() {
public boolean execute(PsiElement element) {
if (element instanceof CssRuleset) {
CssRuleset cssRuleset = (CssRuleset) element;
CssSelectorList selectorList = cssRuleset.getSelectorList();
if (selectorList == null) {
return false; // abort processing
}
String selectorText = StringUtils.normalizeWhitespace(selectorList.getText());
List<List<String>> comparePaths = createSelectorParts(selectorText);
for (List<String> comparePath : comparePaths) {
int numToRemove = 0;
for (List<String> selectorPath : selectorPaths) {
if (endsWith(selectorPath, comparePath)) {
numToRemove = comparePath.size();
for (int i = 0; i < numToRemove; i++) {
pop(selectorPath);
}
selectorPath.add(DUMMY); // Add a dummy marker that won't match again
}
}
for (int i = 0; i < numToRemove; i++) {
pop(comparePath);
}
}
if (cleanupSelectorParts(comparePaths)) {
return false;
}
for (List<String> selectorPath : selectorPaths) {
String stack = pop(selectorPath);// Clear dummy markers
if (!"".equals(stack)) {
selectorPath.add(DUMMY);
return false;
}
}
return true;
}
return true;
}
});
// Check for loose ends in given selector and in code.
if (cleanupSelectorParts(selectorPaths) || !complete) {
return false;
}
return true;
}
/**
* Checks if <tt>match</tt> is equal to the tail of <tt>candidate</tt>
*
* @param candidate the list to test
* @param match the tail to test for
* @return <tt>true</tt> if <tt>match</tt> is a tailing sublist of <tt>candidate</tt>,
* given the semantics of <tt>comparator</tt>
*/
private boolean endsWith(List<String> candidate, List<String> match) {
if (candidate.isEmpty() || match.isEmpty() || match.size() > candidate.size()) {
return false;
}
for (int i = 0; i < match.size(); i++) {
int cix = candidate.size() - 1 - i;
int mix = match.size() - 1 - i;
String s1 = candidate.get(cix);
String s2 = match.get(mix);
if (!s1.equals(s2)) {
if (!(s2.startsWith("&:") && s1.indexOf(':') != -1 && s2.substring(1).equals(s1.substring(s1.indexOf(':'))))) {
return false;
}
candidate.add(cix, s1.substring(0, s1.indexOf(':')));
}
}
return true;
}
/**
* Removes the last element of the given list of strings
*
* @param strings the list to pop
* @return the removed element, or <tt>null</tt> if the list is empty
*/
private String pop(List<String> strings) {
if (!strings.isEmpty()) {
return strings.remove(strings.size() - 1);
}
return null;
}
private List<List<String>> createSelectorParts(String s) {
List<List<String>> parts = new ArrayList<List<String>>();
String[] selectorParts = s.split(",");
for (String part : selectorParts) {
List<String> sub = new ArrayList<String>();
String[] subParts = part.split(" ");
for (String subPart : subParts) {
sub.add(subPart);
}
parts.add(sub);
}
return parts;
}
/**
* Clean up and free memory
*
* @param parts the list to clean
* @return true if there was any strings left
*/
private boolean cleanupSelectorParts(List<List<String>> parts) {
boolean leftovers = false;
for (List<String> part : parts) {
if (!part.isEmpty()) {
leftovers = true;
}
part.clear();
}
parts.clear();
return leftovers;
}
/**
* Get the number of hits this processor has collected
*
* @return the number of collected elements
*/
public int size() {
return selectors.size();
}
/**
* Get the collected elements.
*
* @return the elements collected
* @see #getBlocks()
*/
@NotNull
public CssElement[] getResults() {
return selectors.toArray(new CssElement[selectors.size()]);
}
/**
* Get the corresponding CssBlocks for the collected elements.
*
* @return the blocks for the collected elements
* @see #getResults()
*/
@NotNull
public CssBlock[] getBlocks() {
final Collection<CssBlock> blocks = new ArrayList<CssBlock>();
for (CssElement result : getResults()) {
CssRuleset ruleSet = PsiTreeUtil.getParentOfType(result, CssRuleset.class);
if (ruleSet != null) {
CssBlock block = ruleSet.getBlock();
if (block != null) {
blocks.add(block);
}
}
}
return blocks.toArray(new CssBlock[blocks.size()]);
}
}