blob: b0b74e87c485a6e5feb3cbc46b990bc26fc1bec0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.web.inspect;
import java.awt.EventQueue;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.BadLocationException;
import javax.swing.text.StyledDocument;
import org.netbeans.modules.css.model.api.Model;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.netbeans.modules.web.common.sourcemap.Mapping;
import org.netbeans.modules.web.common.sourcemap.SourceMap;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.cookies.EditorCookie;
import org.openide.cookies.LineCookie;
import org.openide.cookies.OpenCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.text.Line;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.UserQuestionException;
/**
* CSS-related utility methods.
*
* @author Jan Stola
*/
public class CSSUtils {
/** Prefixes of CSS properties used by various vendors. */
private static final String[] vendorPropertyPrefixes = new String[] {
"-moz-", // NOI18N
"-webkit-", // NOI18N
"-ms-", // NOI18N
"-o-" // NOI18N
};
private static final List<String> inheritedProperties = Arrays.asList(new String[] {
"azimuth", // NOI18N
"border-collapse", // NOI18N
"border-spacing", // NOI18N
"caption-side", // NOI18N
"color", // NOI18N
"cursor", // NOI18N
"direction", // NOI18N
"elevation", // NOI18N
"empty-cells", // NOI18N
"font-family", // NOI18N
"font-size", // NOI18N
"font-style", // NOI18N
"font-variant", // NOI18N
"font-weight", // NOI18N
"font", // NOI18N
"letter-spacing", // NOI18N
"line-height", // NOI18N
"list-style-image", // NOI18N
"list-style-position", // NOI18N
"list-style-type", // NOI18N
"list-style", // NOI18N
"orphans", // NOI18N
"pitch-range", // NOI18N
"pitch", // NOI18N
"quotes", // NOI18N
"richness", // NOI18N
"speak-header", // NOI18N
"speak-numeral", // NOI18N
"speak-punctuation", // NOI18N
"speak", // NOI18N
"speech-rate", // NOI18N
"stress", // NOI18N
"text-align", // NOI18N
"text-indent", // NOI18N
"text-transform", // NOI18N
"text-shadow", // NOI18N
"visibility", // NOI18N
"voice-family", // NOI18N
"volume", // NOI18N
"white-space", // NOI18N
"widows", // NOI18N
"word-spacing" // NOI18N
});
/** Name of the class that is used to simulate hovering. */
public static final String HOVER_CLASS = "-netbeans-hover"; // NOI18N
/**
* Determines whether the CSS property with the specified name is inherited.
*
* @param name name of the property.
* @return {@code true} when the property is inherited,
* returns {@code false} otherwise.
*/
public static boolean isInheritedProperty(String name) {
for (String propertyName : possiblePropertyNames(name)) {
if (inheritedProperties.contains(propertyName)) {
return true;
}
}
return false;
}
/**
* Returns possible known (base) names of the given property
* (the returned list contains all candidates with
* possible prefixes/suffixes removed).
*
* @param name name of the property.
* @return list of possible (base) names of the given property.
*/
private static List<String> possiblePropertyNames(String name) {
List<String> names = new ArrayList<String>();
names.add(name);
// -moz-color => color
for (String prefix : vendorPropertyPrefixes) {
if (name.startsWith(prefix)) {
String withoutPrefix = name.substring(prefix.length());
names.add(withoutPrefix);
}
}
return names;
}
/**
* Determines whether the specified CSS value means that the actual
* value should be inherited from the parent.
*
* @param value value to check.
* @return {@code true} if the actual value should be inherited,
* returns {@code false} otherwise.
*/
public static boolean isInheritValue(String value) {
return value.trim().startsWith("inherit"); // NOI18N
}
/**
* Opens the specified file at the given offset.
*
* @param fob file that should be opened.
* @param offset offset where the caret should be placed.
* @return {@code true} when the file was opened successfully,
* returns {@code false} otherwise.
*/
public static boolean openAtOffset(FileObject fob, int offset) {
return openAt(fob, -1, offset);
}
/**
* Opens the specified file at the given line.
*
* @param fob file that should be opened.
* @param lineNo line where the caret should be placed.
* @return {@code true} when the file was opened successfully,
* returns {@code false} otherwise.
*/
public static boolean openAtLine(FileObject fob, int lineNo) {
return openAt(fob, lineNo, 0);
}
/**
* Opens the specified file at the given line and column.
*
* @param fob file that should be opened.
* @param lineNo line where the caret should be placed.
* @param columnNo column where the caret should be placed.
* @return {@code true} when the file was opened successfully,
* returns {@code false} otherwise.
*/
public static boolean openAtLineAndColumn(FileObject fob, int lineNo, int columnNo) {
return openAt(fob, lineNo, columnNo);
}
/**
* Opens the specified file at the given position. The position is either
* offset (when {@code lineNo} is -1) or line/column otherwise.
* This method has been copied (with minor modifications) from UiUtils
* class in csl.api module. This method is not CSS-specific. It was placed
* into this file just because there was no better place.
*
* @param fob file that should be opened.
* @param lineNo line where the caret should be placed
* (or -1 when the {@code columnNo} represents an offset).
* @param columnNo column (or offset when {@code lineNo} is -1)
* where the caret should be placed.
* @return {@code true} when the file was opened successfully,
* returns {@code false} otherwise.
*/
private static boolean openAt(FileObject fob, int lineNo, int columnNo) {
try {
DataObject dob = DataObject.find(fob);
Lookup dobLookup = dob.getLookup();
EditorCookie ec = dobLookup.lookup(EditorCookie.class);
LineCookie lc = dobLookup.lookup(LineCookie.class);
OpenCookie oc = dobLookup.lookup(OpenCookie.class);
if ((ec != null) && (lc != null) && (columnNo != -1)) {
StyledDocument doc;
try {
doc = ec.openDocument();
} catch (UserQuestionException uqe) {
String title = NbBundle.getMessage(
CSSUtils.class,
"CSSUtils.openQuestion"); // NOI18N
Object value = DialogDisplayer.getDefault().notify(new NotifyDescriptor.Confirmation(
uqe.getLocalizedMessage(),
title,
NotifyDescriptor.YES_NO_OPTION));
if (value != NotifyDescriptor.YES_OPTION) {
return false;
}
uqe.confirmed();
doc = ec.openDocument();
}
if (doc != null) {
boolean offset = (lineNo == -1);
int line = offset ? NbDocument.findLineNumber(doc, columnNo) : lineNo;
int column = offset ? columnNo - NbDocument.findLineOffset(doc, line) : columnNo;
if (line != -1) {
Line l = lc.getLineSet().getCurrent(line);
if (l != null) {
l.show(Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS, column);
return true;
}
}
}
}
if (oc != null) {
oc.open();
return true;
}
} catch (IOException ioe) {
Logger.getLogger(CSSUtils.class.getName()).log(Level.INFO, null, ioe);
}
return false;
}
/**
* Returns an unspecified "normalized" version of the selector suitable
* for {@code String} comparison with other normalized selectors.
*
* @param selector selector to normalize.
* @return "normalized" version of the selector.
*/
public static String normalizeSelector(String selector) {
// Hack that simplifies the following cycle: adding a dummy
// character that ensures that the last group is ended.
// This character is removed at the end of this method.
selector += 'A';
String whitespaceChars = " \t\n\r\f\""; // NOI18N
String specialChars = ".>+~#:*()[]|,="; // NOI18N
StringBuilder main = new StringBuilder();
StringBuilder group = null;
for (int i=0; i<selector.length(); i++) {
char c = selector.charAt(i);
boolean whitespace = (whitespaceChars.indexOf(c) != -1);
boolean special = (specialChars.indexOf(c) != -1);
if (whitespace || special) {
if (group == null) {
group = new StringBuilder();
}
if (special) {
group.append(c);
}
} else {
if (group != null) {
if (group.length() == 0) {
// whitespace only group => insert single space instead
main.append(' ');
} else {
// group with special chars
main.append(group);
}
group = null;
}
main.append(c);
}
}
// Removing the dummy character added at the beginning of the method
return main.substring(0, main.length()-1).trim();
}
/**
* Returns an unspecified "normalized" version of the media query suitable
* for {@code String} comparison with other normalized media queries.
*
* @param mediaQueryList media query list to normalize.
* @return "normalized" version of the media query.
*/
public static String normalizeMediaQuery(String mediaQueryList) {
mediaQueryList = mediaQueryList.trim().toLowerCase(Locale.ENGLISH);
StringBuilder result = new StringBuilder();
StringTokenizer st = new StringTokenizer(mediaQueryList, ","); // NOI18N
while (st.hasMoreTokens()) {
String mediaQuery = st.nextToken();
int index;
List<String> parts = new ArrayList<String>();
while ((index = mediaQuery.indexOf("and")) != -1) { // NOI18N
String part = mediaQuery.substring(0,index);
mediaQuery = mediaQuery.substring(index+3);
// 'part' is not a selector, but the same normalization
// works well here as well.
part = normalizeSelector(part);
parts.add(part);
}
mediaQuery = normalizeSelector(mediaQuery);
parts.add(mediaQuery);
Collections.sort(parts);
Collections.reverse(parts); // Make sure that media type is before expressions
for (int i=0; i<parts.size(); i++) {
if (i != 0) {
result.append(" and "); // NOI18N
}
String part = parts.get(i);
result.append(part);
}
if (st.hasMoreTokens()) {
result.append(", "); // NOI18N
}
}
return result.toString();
}
/**
* Determines whether the CSS property with the specified name
* determines some color.
*
* @param propertyName name of the property to check.
* @return {@code true} when the given property determines some color,
* returns {@code false} otherwise.
*/
public static boolean isColorProperty(String propertyName) {
// Simple heuristics
return propertyName.contains("color"); // NOI18N
}
/**
* Jumps into the location given by a source map.
*
* @param cssFile compiled CSS file.
* @param sourceModel source model corresponding to the CSS file.
* @param styleSheetText text of the style-sheet that corresponds to the CSS file.
* @param offset offset in the compiled CSS file.
* @return {@code true} if the file contains source map information
* and this information was used successfully to open the source file,
* returns {@code false} otherwise.
*/
public static boolean goToSourceBySourceMap(FileObject cssFile, Model sourceModel, String styleSheetText, int offset) {
if (styleSheetText != null) {
String sourceMapPath = sourceMapPath(styleSheetText);
if (sourceMapPath != null) {
FileObject folder = cssFile.getParent();
FileObject sourceMapFob = folder.getFileObject(sourceMapPath);
if (sourceMapFob != null) {
try {
CharSequence modelSource = sourceModel.getModelSource();
int line = LexerUtils.getLineOffset(modelSource, offset);
int lineStartOffset = LexerUtils.getLineBeginningOffset(modelSource, line);
int column = offset-lineStartOffset;
String sourceMapText = sourceMapFob.asText();
SourceMap sourceMap = SourceMap.parse(sourceMapText);
final Mapping mapping = sourceMap.findMapping(line, column);
if (mapping == null) {
Logger.getLogger(CSSUtils.class.getName()).log(Level.INFO,
"No mapping for line {0} and column {1}!", new Object[] {line, column}); // NOI18N
} else {
int sourceIndex = mapping.getSourceIndex();
String sourcePath = sourceMap.getSourcePath(sourceIndex);
folder = sourceMapFob.getParent();
final FileObject source = folder.getFileObject(sourcePath);
boolean validSourceFile = (source != null);
if (validSourceFile) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
int line = mapping.getOriginalLine();
int column = mapping.getOriginalColumn();
CSSUtils.openAtLineAndColumn(source, line, column);
}
});
} else {
// Invalid path in the source map.
Logger.getLogger(CSSUtils.class.getName()).log(Level.INFO,
"Unable to find the file {0} relative to the source map {1}!",
new Object[] {sourcePath, sourceMapFob.getPath()});
}
return validSourceFile;
}
} catch (IOException ioex) {
Exceptions.printStackTrace(ioex);
} catch (BadLocationException blex) {
Logger.getLogger(CSSUtils.class.getName()).log(Level.INFO, null, blex);
}
}
}
}
return false;
}
/** Pattern for the source mapping URL in CSS files. */
private static final Pattern SOURCE_MAPPING_PATTERN = Pattern.compile("/\\*[#@]\\s+sourceMappingURL=(.*)\\s+\\*/"); // NOI18N
/**
* Returns the path to the source map.
*
* @param cssText text of some file.
* @return path to the source map or {@code null} if there is no source
* map information in the given text.
*/
public static String sourceMapPath(String cssText) {
String result = null;
if (cssText != null) {
Matcher matcher = SOURCE_MAPPING_PATTERN.matcher(cssText);
if (matcher.find()) {
result = matcher.group(1);
}
}
return result;
}
}