blob: e9de53d23d2e474ec2133a38956c664fc4960853 [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.codeInsight.template.emmet.filters;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.intellij.codeInsight.template.emmet.nodes.GenerationNode;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.Iterables.*;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newLinkedList;
/**
* User: zolotov
* Date: 2/4/13
* <p/>
* Bem filter for emmet support.
* See the original source code here: https://github.com/emmetio/emmet/blob/master/javascript/filters/bem.js
* And documentation here: http://docs.emmet.io/filters/bem/
*/
public class BemEmmetFilter extends ZenCodingFilter {
public static final String SUFFIX = "bem";
private static final Key<BemState> BEM_STATE = Key.create("BEM_STATE");
private static final String ELEMENT_SEPARATOR = "__";
private static final String MODIFIER_SEPARATOR = "_";
private static final String SHORT_ELEMENT_PREFIX = "-";
private static final Joiner ELEMENTS_JOINER = Joiner.on(ELEMENT_SEPARATOR).skipNulls();
private static final Splitter ELEMENTS_SPLITTER = Splitter.on(ELEMENT_SEPARATOR);
private static final Splitter MODIFIERS_SPLITTER = Splitter.on(MODIFIER_SEPARATOR).limit(2);
private static final Splitter CLASS_NAME_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings();
private static final Joiner CLASS_NAME_JOINER = Joiner.on(' ');
private static final Function<String, String> CLASS_NAME_NORMALIZER = new Function<String, String>() {
@NotNull
@Override
public String apply(@NotNull String input) {
if (!input.startsWith(SHORT_ELEMENT_PREFIX)) {
return input;
}
StringBuilder result = new StringBuilder();
while (input.startsWith(SHORT_ELEMENT_PREFIX)) {
input = input.substring(SHORT_ELEMENT_PREFIX.length());
result.append(ELEMENT_SEPARATOR);
}
return result.append(input).toString();
}
};
private static final Predicate<String> BLOCK_NAME_PREDICATE = new Predicate<String>() {
@Override
public boolean apply(String className) {
return Pattern.compile("^[A-z]-").matcher(className).matches();
}
};
private static final Predicate<String> STARTS_WITH_LETTER = new Predicate<String>() {
@Override
public boolean apply(@Nullable String input) {
return input != null && input.length() > 0 && Character.isLetter(input.charAt(0));
}
};
@NotNull
@Override
public String getDisplayName() {
return "BEM";
}
@NotNull
@Override
public String getSuffix() {
return SUFFIX;
}
@Override
public boolean isMyContext(@NotNull PsiElement context) {
return context.getLanguage() instanceof XMLLanguage;
}
@NotNull
@Override
public GenerationNode filterNode(@NotNull final GenerationNode node) {
final List<Couple<String>> attribute2Value = node.getTemplateToken().getAttribute2Value();
Couple<String> classNamePair = getClassPair(attribute2Value);
if (classNamePair != null) {
Iterable<String> classNames = extractClasses(classNamePair.second);
BEM_STATE.set(node, new BemState(suggestBlockName(classNames), null, null));
final Set<String> newClassNames = ImmutableSet.copyOf(concat(transform(classNames, new Function<String, Iterable<String>>() {
@Override
public Iterable<String> apply(String className) {
return processClassName(className, node);
}
})));
attribute2Value.add(Couple.of("class", CLASS_NAME_JOINER.join(newClassNames)));
}
return node;
}
private static Iterable<String> processClassName(String className, @NotNull GenerationNode node) {
className = fillWithBemElements(className, node);
className = fillWithBemModifiers(className, node);
BemState nodeBemState = BEM_STATE.get(node);
BemState bemState = extractBemStateFromClassName(className);
List<String> result = newLinkedList();
if (!bemState.isEmpty()) {
String block = bemState.getBlock();
if (isNullOrEmpty(block)) {
block = nullToEmpty(nodeBemState != null ? nodeBemState.getBlock() : null);
bemState.setBlock(block);
}
String prefix = block;
String element = bemState.getElement();
if (!isNullOrEmpty(element)) {
prefix += ELEMENT_SEPARATOR + element;
}
result.add(prefix);
String modifier = bemState.getModifier();
if (!isNullOrEmpty(modifier)) {
result.add(prefix + MODIFIER_SEPARATOR + modifier);
}
BEM_STATE.set(node, bemState.copy());
}
else {
result.add(className);
}
return result;
}
@NotNull
private static BemState extractBemStateFromClassName(@NotNull String className) {
final BemState result = new BemState();
if (className.contains(ELEMENT_SEPARATOR)) {
final Iterator<String> elementsIterator = ELEMENTS_SPLITTER.split(className).iterator();
result.setBlock(elementsIterator.next());
final Collection<String> elementParts = newLinkedList();
while (elementsIterator.hasNext()) {
final String elementPart = elementsIterator.next();
if (!elementsIterator.hasNext()) {
final List<String> elementModifiers = newArrayList(MODIFIERS_SPLITTER.split(elementPart));
elementParts.add(getFirst(elementModifiers, null));
if (elementModifiers.size() > 1) {
result.setModifier(getLast(elementModifiers, ""));
}
}
else {
elementParts.add(elementPart);
}
}
result.setElement(ELEMENTS_JOINER.join(elementParts));
}
else if (className.contains(MODIFIER_SEPARATOR)) {
final Iterable<String> blockModifiers = MODIFIERS_SPLITTER.split(className);
result.setBlock(getFirst(blockModifiers, ""));
result.setModifier(getLast(blockModifiers, ""));
}
return result;
}
@NotNull
private static String fillWithBemElements(@NotNull String className, @NotNull GenerationNode node) {
return transformClassNameToBemFormat(className, ELEMENT_SEPARATOR, node);
}
@NotNull
private static String fillWithBemModifiers(@NotNull String className, @NotNull GenerationNode node) {
return transformClassNameToBemFormat(className, MODIFIER_SEPARATOR, node);
}
/**
* Adduction className to BEM format according to tags structure.
*
* @param className
* @param separator handling separator
* @param node current node
* @return class name in BEM format
*/
@NotNull
private static String transformClassNameToBemFormat(@NotNull String className, @NotNull String separator, @NotNull GenerationNode node) {
Pair<String, Integer> cleanStringAndDepth = getCleanStringAndDepth(className, separator);
Integer depth = cleanStringAndDepth.second;
if (depth > 0) {
GenerationNode donor = node;
while (donor.getParent() != null && depth > 0) {
donor = donor.getParent();
depth--;
}
BemState bemState = BEM_STATE.get(donor);
if (bemState != null) {
String prefix = bemState.getBlock();
if (!isNullOrEmpty(prefix)) {
String element = bemState.getElement();
if (MODIFIER_SEPARATOR.equals(separator) && !isNullOrEmpty(element)) {
prefix = prefix + separator + element;
}
return prefix + separator + cleanStringAndDepth.first;
}
}
}
return className;
}
/**
* Counts separators at the start of className and retrieve className without these separators.
*
* @param name
* @param separator
* @return pair like <name_without_separator_at_the_start, count_of_separators_at_the_start_of_string>
*/
@NotNull
private static Pair<String, Integer> getCleanStringAndDepth(@NotNull String name, @NotNull String separator) {
int result = 0;
while (name.startsWith(separator)) {
result++;
name = name.substring(separator.length());
}
return Pair.create(name, result);
}
/**
* Extract all normalized class names from class attribute value
*
* @param classAttributeValue
* @return collection of normalized class names
*/
@NotNull
private static Iterable<String> extractClasses(String classAttributeValue) {
return transform(CLASS_NAME_SPLITTER.split(classAttributeValue), CLASS_NAME_NORMALIZER);
}
/**
* Suggest block name by class names.
* Returns first class started with pattern [a-z]-
* or first class started with letter.
*
* @param classNames
* @return suggested block name for given classes. Empty string if name can't be suggested.
*/
@NotNull
private static String suggestBlockName(Iterable<String> classNames) {
return find(classNames, BLOCK_NAME_PREDICATE, find(classNames, STARTS_WITH_LETTER, ""));
}
/**
* Retrieve pair "class" => classAttributeValue from node attributeList
*
* @param attribute2Value node's attributes
* @return pointer to pair
*/
@Nullable
private static Couple<String> getClassPair(@NotNull List<Couple<String>> attribute2Value) {
for (int i = 0; i < attribute2Value.size(); i++) {
Couple<String> pair = attribute2Value.get(i);
if ("class".equals(pair.first) && !isNullOrEmpty(pair.second)) {
return attribute2Value.remove(i);
}
}
return null;
}
private static class BemState {
@Nullable private String block;
@Nullable private String element;
@Nullable private String modifier;
private BemState() {
}
private BemState(@Nullable String block, @Nullable String element, @Nullable String modifier) {
this.block = block;
this.element = element;
this.modifier = modifier;
}
public void setModifier(@Nullable String modifier) {
this.modifier = modifier;
}
public void setElement(@Nullable String element) {
this.element = element;
}
public void setBlock(@Nullable String block) {
this.block = block;
}
@Nullable
public String getBlock() {
return block;
}
@Nullable
public String getElement() {
return element;
}
@Nullable
public String getModifier() {
return modifier;
}
public boolean isEmpty() {
return isNullOrEmpty(block) && isNullOrEmpty(element) && isNullOrEmpty(modifier);
}
@Nullable
public BemState copy() {
return new BemState(block, element, modifier);
}
}
}