/*
* Copyright (c) 2008-2013, Matthias Mann
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Matthias Mann nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package de.matthiasmann.twl.textarea;
import de.matthiasmann.twl.utils.StringList;
import de.matthiasmann.twl.Color;
import de.matthiasmann.twl.utils.ParameterStringParser;
import de.matthiasmann.twl.utils.TextUtil;
import java.util.HashMap;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Style which is constructed from a CSS style string.
*
* @author Matthias Mann
*/
public class CSSStyle extends Style {
protected CSSStyle() {
}
public CSSStyle(String cssStyle) {
parseCSS(cssStyle);
}
public CSSStyle(Style parent, StyleSheetKey styleSheetKey, String cssStyle) {
super(parent, styleSheetKey);
parseCSS(cssStyle);
}
private void parseCSS(String style) {
ParameterStringParser psp = new ParameterStringParser(style, ';', ':');
psp.setTrim(true);
while(psp.next()) {
try {
parseCSSAttribute(psp.getKey(), psp.getValue());
} catch(IllegalArgumentException ex) {
Logger.getLogger(CSSStyle.class.getName()).log(Level.SEVERE,
"Unable to parse CSS attribute: " + psp.getKey() + "=" + psp.getValue(), ex);
}
}
}
protected void parseCSSAttribute(String key, String value) {
if(key.startsWith("margin")) {
parseBox(key.substring(6), value, StyleAttribute.MARGIN);
return;
}
if(key.startsWith("padding")) {
parseBox(key.substring(7), value, StyleAttribute.PADDING);
return;
}
if(key.startsWith("font")) {
parseFont(key, value);
return;
}
if("text-indent".equals(key)) {
parseValueUnit(StyleAttribute.TEXT_INDENT, value);
return;
}
if("-twl-font".equals(key)) {
put(StyleAttribute.FONT_FAMILIES, new StringList(value));
return;
}
if("-twl-hover".equals(key)) {
parseEnum(StyleAttribute.INHERIT_HOVER, INHERITHOVER, value);
return;
}
if("text-align".equals(key)) {
parseEnum(StyleAttribute.HORIZONTAL_ALIGNMENT, value);
return;
}
if("text-decoration".equals(key)) {
parseEnum(StyleAttribute.TEXT_DECORATION, TEXTDECORATION, value);
return;
}
if("vertical-align".equals(key)) {
parseEnum(StyleAttribute.VERTICAL_ALIGNMENT, value);
return;
}
if("white-space".equals(key)) {
parseEnum(StyleAttribute.PREFORMATTED, PRE, value);
return;
}
if("word-wrap".equals(key)) {
parseEnum(StyleAttribute.BREAKWORD, BREAKWORD, value);
return;
}
if("list-style-image".equals(key)) {
parseURL(StyleAttribute.LIST_STYLE_IMAGE, value);
return;
}
if("list-style-type".equals(key)) {
parseEnum(StyleAttribute.LIST_STYLE_TYPE, OLT, value);
return;
}
if("clear".equals(key)) {
parseEnum(StyleAttribute.CLEAR, value);
return;
}
if("float".equals(key)) {
parseEnum(StyleAttribute.FLOAT_POSITION, value);
return;
}
if("display".equals(key)) {
parseEnum(StyleAttribute.DISPLAY, value);
return;
}
if("width".equals(key)) {
parseValueUnit(StyleAttribute.WIDTH, value);
return;
}
if("height".equals(key)) {
parseValueUnit(StyleAttribute.HEIGHT, value);
return;
}
if("background-image".equals(key)) {
parseURL(StyleAttribute.BACKGROUND_IMAGE, value);
return;
}
if("background-color".equals(key) || "-twl-background-color".equals(key)) {
parseColor(StyleAttribute.BACKGROUND_COLOR, value);
return;
}
if("color".equals(key)) {
parseColor(StyleAttribute.COLOR, value);
return;
}
if("tab-size".equals(key) || "-moz-tab-size".equals(key)) {
parseInteger(StyleAttribute.TAB_SIZE, value);
return;
}
throw new IllegalArgumentException("Unsupported key: " + key);
}
private void parseBox(String key, String value, BoxAttribute box) {
if("-top".equals(key)) {
parseValueUnit(box.top, value);
} else if("-left".equals(key)) {
parseValueUnit(box.left, value);
} else if("-right".equals(key)) {
parseValueUnit(box.right, value);
} else if("-bottom".equals(key)) {
parseValueUnit(box.bottom, value);
} else if("".equals(key)) {
Value[] vu = parseValueUnits(value);
switch(vu.length) {
case 1:
put(box.top, vu[0]);
put(box.left, vu[0]);
put(box.right, vu[0]);
put(box.bottom, vu[0]);
break;
case 2: // TB, LR
put(box.top, vu[0]);
put(box.left, vu[1]);
put(box.right, vu[1]);
put(box.bottom, vu[0]);
break;
case 3: // T, LR, B
put(box.top, vu[0]);
put(box.left, vu[1]);
put(box.right, vu[1]);
put(box.bottom, vu[2]);
break;
case 4: // T, R, B, L
put(box.top, vu[0]);
put(box.left, vu[3]);
put(box.right, vu[1]);
put(box.bottom, vu[2]);
break;
default:
throw new IllegalArgumentException("Invalid number of margin values: " + vu.length);
}
}
}
private void parseFont(String key, String value) {
if("font-family".equals(key)) {
parseList(StyleAttribute.FONT_FAMILIES, value);
return;
}
if("font-weight".equals(key)) {
Integer weight = WEIGHTS.get(value);
if(weight == null) {
weight = Integer.valueOf(value);
}
put(StyleAttribute.FONT_WEIGHT, weight);
return;
}
if("font-size".equals(key)) {
parseValueUnit(StyleAttribute.FONT_SIZE, value);
return;
}
if("font-style".equals(key)) {
parseEnum(StyleAttribute.FONT_ITALIC, ITALIC, value);
return;
}
if("font".equals(key)) {
value = parseStartsWith(StyleAttribute.FONT_WEIGHT, WEIGHTS, value);
value = parseStartsWith(StyleAttribute.FONT_ITALIC, ITALIC, value);
if(value.length() > 0 && Character.isDigit(value.charAt(0))) {
int end = TextUtil.indexOf(value, ' ', 0);
parseValueUnit(StyleAttribute.FONT_SIZE, value.substring(0, end));
end = TextUtil.skipSpaces(value, end);
value = value.substring(end);
}
parseList(StyleAttribute.FONT_FAMILIES, value);
}
}
private Value parseValueUnit(String value) {
Value.Unit unit;
int suffixLength = 2;
if(value.endsWith("px")) {
unit = Value.Unit.PX;
} else if(value.endsWith("pt")) {
unit = Value.Unit.PT;
} else if(value.endsWith("em")) {
unit = Value.Unit.EM;
} else if(value.endsWith("ex")) {
unit = Value.Unit.EX;
} else if(value.endsWith("%")) {
suffixLength = 1;
unit = Value.Unit.PERCENT;
} else if("0".equals(value)) {
return Value.ZERO_PX;
} else if("auto".equals(value)) {
return Value.AUTO;
} else {
throw new IllegalArgumentException("Unknown numeric suffix: " + value);
}
String numberPart = TextUtil.trim(value, 0, value.length() - suffixLength);
return new Value(Float.parseFloat(numberPart), unit);
}
private Value[] parseValueUnits(String value) {
String[] parts = value.split("\\s+");
Value[] result = new Value[parts.length];
for(int i=0 ; i<parts.length ; i++) {
result[i] = parseValueUnit(parts[i]);
}
return result;
}
private void parseValueUnit(StyleAttribute<?> attribute, String value) {
put(attribute, parseValueUnit(value));
}
private void parseInteger(StyleAttribute<Integer> attribute, String value) {
if("inherit".equals(value)) {
put(attribute, null);
} else {
int intval = Integer.parseInt(value);
put(attribute, intval);
}
}
private<T> void parseEnum(StyleAttribute<T> attribute, HashMap<String, T> map, String value) {
T obj = map.get(value);
if(obj == null) {
throw new IllegalArgumentException("Unknown value: " + value);
}
put(attribute, obj);
}
private<E extends Enum<E>> void parseEnum(StyleAttribute<E> attribute, String value) {
E obj = Enum.valueOf(attribute.getDataType(), value.toUpperCase(Locale.ENGLISH));
put(attribute, obj);
}
private<E> String parseStartsWith(StyleAttribute<E> attribute, HashMap<String, E> map, String value) {
int end = TextUtil.indexOf(value, ' ', 0);
E obj = map.get(value.substring(0, end));
if(obj != null) {
end = TextUtil.skipSpaces(value, end);
value = value.substring(end);
}
put(attribute, obj);
return value;
}
private void parseURL(StyleAttribute<String> attribute, String value) {
put(attribute, stripURL(value));
}
static String stripTrim(String value, int start, int end) {
return TextUtil.trim(value, start, value.length() - end);
}
static String stripURL(String value) {
if(value.startsWith("url(") && value.endsWith(")")) {
value = stripQuotes(stripTrim(value, 4, 1));
}
return value;
}
static String stripQuotes(String value) {
if((value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length() - 1);
}
return value;
}
private void parseColor(StyleAttribute<Color> attribute, String value) {
Color color;
if(value.startsWith("rgb(") && value.endsWith(")")) {
value = stripTrim(value, 4, 1);
byte[] rgb = parseRGBA(value, 3);
color = new Color(rgb[0], rgb[1], rgb[2], (byte)255);
} else if(value.startsWith("rgba(") && value.endsWith(")")) {
value = stripTrim(value, 5, 1);
byte[] rgba = parseRGBA(value, 4);
color = new Color(rgba[0], rgba[1], rgba[2], rgba[3]);
} else {
color = Color.parserColor(value);
if(color == null) {
throw new IllegalArgumentException("unknown color name: " + value);
}
}
put(attribute, color);
}
private byte[] parseRGBA(String value, int numElements) {
String[] parts = value.split(",");
if(parts.length != numElements) {
throw new IllegalArgumentException("3 values required for rgb()");
}
byte[] rgba = new byte[numElements];
for(int i=0 ; i<numElements ; i++) {
String part = parts[i].trim();
int v;
if(i == 3) {
// handle alpha component specially
float f = Float.parseFloat(part);
v = Math.round(f * 255.0f);
} else {
boolean percent = part.endsWith("%");
if(percent) {
part = stripTrim(value, 0, 1);
}
v = Integer.parseInt(part);
if(percent) {
v = 255*v / 100;
}
}
rgba[i] = (byte)Math.max(0, Math.min(255, v));
}
return rgba;
}
private void parseList(StyleAttribute<StringList> attribute, String value) {
put(attribute, parseList(value, 0));
}
static StringList parseList(String value, int idx) {
idx = TextUtil.skipSpaces(value, idx);
if(idx >= value.length()) {
return null;
}
char startChar = value.charAt(idx);
int end;
String part;
if(startChar == '"' || startChar == '\'') {
++idx;
end = TextUtil.indexOf(value, startChar, idx);
part = value.substring(idx, end);
end = TextUtil.skipSpaces(value, ++end);
if(end < value.length() && value.charAt(end) != ',') {
throw new IllegalArgumentException("',' expected at " + idx);
}
} else {
end = TextUtil.indexOf(value, ',', idx);
part = TextUtil.trim(value, idx, end);
}
return new StringList(part, parseList(value, end+1));
}
static final HashMap<String, Boolean> PRE = new HashMap<String, Boolean>();
static final HashMap<String, Boolean> BREAKWORD = new HashMap<String, Boolean>();
static final HashMap<String, OrderedListType> OLT = new HashMap<String, OrderedListType>();
static final HashMap<String, Boolean> ITALIC = new HashMap<String, Boolean>();
static final HashMap<String, Integer> WEIGHTS = new HashMap<String, Integer>();
static final HashMap<String, TextDecoration> TEXTDECORATION = new HashMap<String, TextDecoration>();
static final HashMap<String, Boolean> INHERITHOVER = new HashMap<String, Boolean>();
static OrderedListType createRoman(final boolean lowercase) {
return new OrderedListType() {
@Override
public String format(int nr) {
if(nr >= 1 && nr <= TextUtil.MAX_ROMAN_INTEGER) {
String str = TextUtil.toRomanNumberString(nr);
return lowercase ? str.toLowerCase(Locale.ENGLISH) : str;
} else {
return Integer.toString(nr);
}
}
};
}
static {
PRE.put("pre", Boolean.TRUE);
PRE.put("normal", Boolean.FALSE);
BREAKWORD.put("normal", Boolean.FALSE);
BREAKWORD.put("break-word", Boolean.TRUE);
OrderedListType upper_alpha = new OrderedListType("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
OrderedListType lower_alpha = new OrderedListType("abcdefghijklmnopqrstuvwxyz");
OLT.put("decimal", OrderedListType.DECIMAL);
OLT.put("upper-alpha", upper_alpha);
OLT.put("lower-alpha", lower_alpha);
OLT.put("upper-latin", upper_alpha);
OLT.put("lower-latin", lower_alpha);
OLT.put("upper-roman", createRoman(false));
OLT.put("lower-roman", createRoman(true));
OLT.put("lower-greek", new OrderedListType("αβγδεζηθικλμνξοπρστυφχψω"));
OLT.put("upper-norwegian", new OrderedListType("ABCDEFGHIJKLMNOPQRSTUVWXYZÆØÅ"));
OLT.put("lower-norwegian", new OrderedListType("abcdefghijklmnopqrstuvwxyzæøå"));
OLT.put("upper-russian-short", new OrderedListType("АБВГДЕЖЗИКЛМНОПРСТУФХЦЧШЩЭЮЯ"));
OLT.put("lower-russian-short", new OrderedListType("абвгдежзиклмнопрстуфхцчшщэюя"));
ITALIC.put("normal", Boolean.FALSE);
ITALIC.put("italic", Boolean.TRUE);
ITALIC.put("oblique", Boolean.TRUE);
WEIGHTS.put("normal", 400);
WEIGHTS.put("bold", 700);
TEXTDECORATION.put("none", TextDecoration.NONE);
TEXTDECORATION.put("underline", TextDecoration.UNDERLINE);
TEXTDECORATION.put("line-through", TextDecoration.LINE_THROUGH);
INHERITHOVER.put("inherit", Boolean.TRUE);
INHERITHOVER.put("normal", Boolean.FALSE);
}
}