001: /*
002: * Copyright 2000,2005 wingS development team.
003: *
004: * This file is part of wingS (http://wingsframework.org).
005: *
006: * wingS is free software; you can redistribute it and/or modify
007: * it under the terms of the GNU Lesser General Public License
008: * as published by the Free Software Foundation; either version 2.1
009: * of the License, or (at your option) any later version.
010: *
011: * Please see COPYING for the complete licence.
012: */
013: package org.wings.style;
014:
015: import org.wings.SFont;
016: import org.wings.io.Device;
017: import org.wings.util.SStringBuilder;
018: import org.apache.commons.logging.Log;
019: import org.apache.commons.logging.LogFactory;
020:
021: import java.awt.*;
022: import java.io.*;
023: import java.net.MalformedURLException;
024: import java.net.URL;
025: import java.util.*;
026: import java.util.List;
027:
028: public class CSSStyleSheet implements StyleSheet {
029: /**
030: * Apache jakarta commons logger
031: */
032: private final static Log log = LogFactory
033: .getLog(CSSStyleSheet.class);
034:
035: private static final Map<String, Float> lengthMapping = new HashMap<String, Float>();
036: static {
037: lengthMapping.put("pt", new Float(1f));
038: lengthMapping.put("px", new Float(1.3f));
039: lengthMapping.put("mm", new Float(2.83464f));
040: lengthMapping.put("cm", new Float(28.3464f));
041: lengthMapping.put("pc", new Float(12f));
042: lengthMapping.put("in", new Float(72f));
043: }
044:
045: private final Map<Selector, Style> map;
046:
047: /**
048: * Constructs an empty style sheet.
049: */
050: public CSSStyleSheet() {
051: map = new HashMap<Selector, Style>();
052: }
053:
054: /**
055: * Constructs a new style sheet instance by parsing the passed input stream into {@link #read(java.io.InputStream)}.
056: * @param in Input stream containing a valid CSS style sheet file.
057: */
058: public CSSStyleSheet(InputStream in) throws IOException {
059: this ();
060: read(in);
061: }
062:
063: public void putStyle(Style style) {
064: map.put(style.getSelector(), style);
065: //style.setSheet(this);
066: }
067:
068: public Set<Style> styles() {
069: return new HashSet<Style>(map.values());
070: }
071:
072: /**
073: * Write each style in set to the device.
074: */
075: public void write(Device out) throws IOException {
076: for (Map.Entry<Selector, Style> entry1 : map.entrySet()) {
077: Map.Entry entry = (Map.Entry) entry1;
078: ((Style) entry.getValue()).write(out);
079: }
080: out.flush();
081: }
082:
083: /**
084: * Creates styles by parsing an input stream.
085: * @param inStream Stream containing style sheet source
086: */
087: public void read(InputStream inStream) throws IOException {
088: Reader r = new BufferedReader(new InputStreamReader(inStream));
089: CssParser parser = new CssParser();
090: parser.parse(null, r, false, false);
091: }
092:
093: /**
094: * Reads and imports a style sheet file accessible via the passed URL.
095: * @param url The url of the style sheet file.
096: */
097: public void importStyleSheet(URL url) {
098: try {
099: InputStream is = url.openStream();
100: Reader r = new BufferedReader(new InputStreamReader(is));
101: CssParser parser = new CssParser();
102: parser.parse(url, r, false, true);
103: r.close();
104: is.close();
105: } catch (Throwable e) {
106: log.warn("Unable to import CSS defintions from " + url, e);
107: // on error we simply have no styles... the html
108: // will look mighty wrong but still function.
109: }
110: }
111:
112: public boolean isFinal() {
113: return false;
114: }
115:
116: /**
117: * Fetches the font to use for the given set of attributes.
118: */
119: public static SFont getFont(CSSAttributeSet a) {
120: boolean anyFontAttribute = false;
121: int size = getFontSize(a);
122: anyFontAttribute |= (size > 0);
123:
124: /*
125: * If the vertical alignment is set to either superscirpt or
126: * subscript we reduce the font size by 2 points.
127: */
128: String vAlign = a.get(CSSProperty.VERTICAL_ALIGN);
129:
130: if (vAlign != null) {
131: if ((vAlign.indexOf("sup") >= 0)
132: || (vAlign.indexOf("sub") >= 0)) {
133: size -= 2;
134: }
135: }
136:
137: String family = a.get(CSSProperty.FONT_FAMILY);
138: anyFontAttribute |= (family != null);
139:
140: int style = Font.PLAIN;
141: String weight = a.get(CSSProperty.FONT_WEIGHT);
142: if (weight == null)
143: ;
144: else if (weight.equals("bold")) {
145: style |= Font.BOLD;
146: } else if (weight.equals("normal"))
147: ;
148: else {
149: try {
150: int w = Integer.parseInt(weight);
151: if (w > 400)
152: style |= Font.BOLD;
153: } catch (NumberFormatException nfe) {
154: }
155: }
156: anyFontAttribute |= (weight != null);
157:
158: String styleValue = a.get(CSSProperty.FONT_STYLE);
159: if ((styleValue != null)
160: && (styleValue.toLowerCase().indexOf("italic") >= 0))
161: style |= Font.ITALIC;
162: anyFontAttribute |= (styleValue != null);
163: return anyFontAttribute ? new SFont(family, style, size) : null;
164: }
165:
166: public static Insets getInsets(CSSAttributeSet a) {
167: String top = a.get(CSSProperty.PADDING_TOP);
168: String left = a.get(CSSProperty.PADDING_LEFT);
169: String bottom = a.get(CSSProperty.PADDING_BOTTOM);
170: String right = a.get(CSSProperty.PADDING_RIGHT);
171: if (top != null || left != null || bottom != null
172: || right != null)
173: return new Insets(length(top), length(left),
174: length(bottom), length(right));
175: else
176: return null;
177: }
178:
179: public static CSSAttributeSet getAttributes(Insets insets) {
180: CSSAttributeSet attributes = new CSSAttributeSet();
181: attributes.put(CSSProperty.PADDING_TOP, insets.top + "px");
182: attributes.put(CSSProperty.PADDING_LEFT, insets.left + "px");
183: attributes
184: .put(CSSProperty.PADDING_BOTTOM, insets.bottom + "px");
185: attributes.put(CSSProperty.PADDING_RIGHT, insets.right + "px");
186: return attributes;
187: }
188:
189: private static int length(String lengthString) {
190: if (lengthString == null || lengthString.length() == 0)
191: return 0;
192: else
193: return Integer.parseInt(lengthString);
194: }
195:
196: static final int sizeMap[] = { 8, 10, 12, 14, 18, 24, 36 };
197:
198: /**
199: * parses the font size attribute. return -1, if no font size
200: * is specified.
201: */
202: private static int getFontSize(CSSAttributeSet attr) {
203: String value = attr.get(CSSProperty.FONT_SIZE);
204: if (value == null)
205: return -1;
206: try {
207: if (value.equals("xx-small")) {
208: return 8;
209: } else if (value.equals("x-small")) {
210: return 10;
211: } else if (value.equals("small")) {
212: return 12;
213: } else if (value.equals("medium")) {
214: return 14;
215: } else if (value.equals("large")) {
216: return 18;
217: } else if (value.equals("x-large")) {
218: return 24;
219: } else if (value.equals("xx-large")) {
220: return 36;
221: } else {
222: return new Float(getLength(value)).intValue();
223: }
224: } catch (NumberFormatException nfe) {
225: return -1;
226: }
227: }
228:
229: public static float getLength(String value) {
230: int length = value.length();
231: if (length >= 2) {
232: String units = value.substring(length - 2, length);
233: Float scale = (Float) lengthMapping.get(units);
234:
235: if (scale != null) {
236: try {
237: return Float
238: .valueOf(value.substring(0, length - 2))
239: .floatValue()
240: * scale.floatValue();
241: } catch (NumberFormatException nfe) {
242: }
243: } else {
244: // treat like points.
245: try {
246: return Float.valueOf(value).floatValue();
247: } catch (NumberFormatException nfe) {
248: }
249: }
250: } else if (length > 0) {
251: // treat like points.
252: try {
253: return Float.valueOf(value).floatValue();
254: } catch (NumberFormatException nfe) {
255: }
256: }
257: return 12.0f;
258: }
259:
260: /**
261: * Takes a set of attributes and turn it into a foreground color
262: * specification. This might be used to specify things
263: * like brighter, more hue, etc.
264: *
265: * @param a the set of attributes
266: * @return the color
267: */
268: public static Color getForeground(CSSAttributeSet a) {
269: return getColor(a, CSSProperty.COLOR);
270: }
271:
272: /**
273: * Takes a set of attributes and turn it into a background color
274: * specification. This might be used to specify things
275: * like brighter, more hue, etc.
276: *
277: * @param a the set of attributes
278: * @return the color
279: */
280: public static Color getBackground(CSSAttributeSet a) {
281: return getColor(a, CSSProperty.BACKGROUND_COLOR);
282: }
283:
284: static Color getColor(CSSAttributeSet a, CSSProperty cssProperty) {
285: String cv = a.get(cssProperty);
286: if (cv != null) {
287: return stringToColor(cv);
288: }
289: return null;
290: }
291:
292: /**
293: * Converts a type Color to a hex string
294: * in the format "#RRGGBB"
295: */
296: static String colorToHex(Color color) {
297: String colorstr = "#";
298:
299: // Red
300: String str = Integer.toHexString(color.getRed());
301: if (str.length() > 2)
302: str = str.substring(0, 2);
303: if (str.length() < 2)
304: colorstr += "0" + str;
305: else
306: colorstr += str;
307:
308: // Green
309: str = Integer.toHexString(color.getGreen());
310: if (str.length() > 2)
311: str = str.substring(0, 2);
312: if (str.length() < 2)
313: colorstr += "0" + str;
314: else
315: colorstr += str;
316:
317: // Blue
318: str = Integer.toHexString(color.getBlue());
319: if (str.length() > 2)
320: str = str.substring(0, 2);
321: if (str.length() < 2)
322: colorstr += "0" + str;
323: else
324: colorstr += str;
325:
326: return colorstr;
327: }
328:
329: /**
330: * Convert a "#FFFFFF" hex string to a Color.
331: * If the color specification is bad, an attempt
332: * will be made to fix it up.
333: */
334: static final Color hexToColor(String value) {
335: String digits;
336: if (value.startsWith("#")) {
337: digits = value.substring(1, Math.min(value.length(), 7));
338: } else {
339: digits = value;
340: }
341: String hstr = "0x" + digits;
342: Color c;
343: try {
344: c = Color.decode(hstr);
345: } catch (NumberFormatException nfe) {
346: c = null;
347: }
348: return c;
349: }
350:
351: /**
352: * Convert a color string such as "RED" or "#NNNNNN" or "rgb(r, g, b)"
353: * to a Color.
354: */
355: static Color stringToColor(String str) {
356: Color color = null;
357:
358: if (str.length() == 0)
359: color = Color.black;
360: else if (str.startsWith("rgb(")) {
361: color = parseRGB(str);
362: } else if (str.charAt(0) == '#')
363: color = hexToColor(str);
364: else if (str.equalsIgnoreCase("Black"))
365: color = hexToColor("#000000");
366: else if (str.equalsIgnoreCase("Silver"))
367: color = hexToColor("#C0C0C0");
368: else if (str.equalsIgnoreCase("Gray"))
369: color = hexToColor("#808080");
370: else if (str.equalsIgnoreCase("White"))
371: color = hexToColor("#FFFFFF");
372: else if (str.equalsIgnoreCase("Maroon"))
373: color = hexToColor("#800000");
374: else if (str.equalsIgnoreCase("Red"))
375: color = hexToColor("#FF0000");
376: else if (str.equalsIgnoreCase("Purple"))
377: color = hexToColor("#800080");
378: else if (str.equalsIgnoreCase("Fuchsia"))
379: color = hexToColor("#FF00FF");
380: else if (str.equalsIgnoreCase("Green"))
381: color = hexToColor("#008000");
382: else if (str.equalsIgnoreCase("Lime"))
383: color = hexToColor("#00FF00");
384: else if (str.equalsIgnoreCase("Olive"))
385: color = hexToColor("#808000");
386: else if (str.equalsIgnoreCase("Yellow"))
387: color = hexToColor("#FFFF00");
388: else if (str.equalsIgnoreCase("Navy"))
389: color = hexToColor("#000080");
390: else if (str.equalsIgnoreCase("Blue"))
391: color = hexToColor("#0000FF");
392: else if (str.equalsIgnoreCase("Teal"))
393: color = hexToColor("#008080");
394: else if (str.equalsIgnoreCase("Aqua"))
395: color = hexToColor("#00FFFF");
396: else
397: color = hexToColor(str); // sometimes get specified without leading #
398: return color;
399: }
400:
401: /**
402: * Parses a String in the format <code>rgb(r, g, b)</code> where
403: * each of the Color components is either an integer, or a floating number
404: * with a % after indicating a percentage value of 255. Values are
405: * constrained to fit with 0-255. The resulting Color is returned.
406: */
407: private static Color parseRGB(String string) {
408: // Find the next numeric char
409: int[] index = new int[1];
410:
411: index[0] = 4;
412: int red = getColorComponent(string, index);
413: int green = getColorComponent(string, index);
414: int blue = getColorComponent(string, index);
415:
416: return new Color(red, green, blue);
417: }
418:
419: /**
420: * Returns the next integer value from <code>string</code> starting
421: * at <code>index[0]</code>. The value can either can an integer, or
422: * a percentage (floating number ending with %), in which case it is
423: * multiplied by 255.
424: */
425: private static int getColorComponent(String string, int[] index) {
426: int length = string.length();
427: char aChar;
428:
429: // Skip non-decimal chars
430: while (index[0] < length
431: && (aChar = string.charAt(index[0])) != '-'
432: && !Character.isDigit(aChar) && aChar != '.') {
433: index[0]++;
434: }
435:
436: int start = index[0];
437:
438: if (start < length && string.charAt(index[0]) == '-') {
439: index[0]++;
440: }
441: while (index[0] < length
442: && Character.isDigit(string.charAt(index[0]))) {
443: index[0]++;
444: }
445: if (index[0] < length && string.charAt(index[0]) == '.') {
446: // Decimal value
447: index[0]++;
448: while (index[0] < length
449: && Character.isDigit(string.charAt(index[0]))) {
450: index[0]++;
451: }
452: }
453: if (start != index[0]) {
454: try {
455: float value = Float.parseFloat(string.substring(start,
456: index[0]));
457:
458: if (index[0] < length && string.charAt(index[0]) == '%') {
459: index[0]++;
460: value = value * 255f / 100f;
461: }
462: return Math.min(255, Math.max(0, (int) value));
463: } catch (NumberFormatException nfe) {
464: // Treat as 0
465: }
466: }
467: return 0;
468: }
469:
470: public static URL getURL(URL base, String cssString) {
471: if (cssString == null) {
472: return null;
473: }
474: if (cssString.startsWith("url(") && cssString.endsWith(")")) {
475: cssString = cssString.substring(4, cssString.length() - 1);
476: }
477: // Absolute first
478: try {
479: return new URL(cssString);
480: } catch (MalformedURLException mue) {
481: }
482: // Then relative
483: if (base != null) {
484: // Relative URL, try from base
485: try {
486: return new URL(base, cssString);
487: } catch (MalformedURLException muee) {
488: }
489: }
490: return null;
491: }
492:
493: public static CSSAttributeSet getAttributes(SFont font) {
494: CSSAttributeSet attributes = new CSSAttributeSet();
495: if (font == null)
496: return attributes;
497:
498: String face = font.getFace();
499: if (face != null && face.length() == 0)
500: face = null;
501:
502: boolean italic = (font.getStyle() & Font.ITALIC) > 0;
503: boolean bold = (font.getStyle() & Font.BOLD) > 0;
504: int size = font.getSize();
505:
506: if (face != null && size != -1) {
507: // use font property
508: SStringBuilder builder = new SStringBuilder();
509: if (italic) {
510: builder.append("italic ");
511: }
512: if (bold) {
513: builder.append("bold ");
514: }
515: if (size > 0) {
516: builder.append(size);
517: builder.append("pt ");
518: }
519: if (face != null && face.length() > 0) {
520: builder.append(face);
521: }
522: attributes.put(CSSProperty.FONT, builder.toString());
523: } else {
524: // use special properties
525: if (italic)
526: attributes.put(CSSProperty.FONT_STYLE, "italic");
527: if (bold)
528: attributes.put(CSSProperty.FONT_WEIGHT, "bold");
529: if (size > -1)
530: attributes.put(CSSProperty.FONT_SIZE, size + "pt");
531: if (face != null)
532: attributes.put(CSSProperty.FONT_FAMILY, face);
533: }
534: return attributes;
535: }
536:
537: public static CSSAttributeSet getAttributes(Color color,
538: CSSProperty cssProperty) {
539: CSSAttributeSet attributes = new CSSAttributeSet();
540: if (color != null)
541: attributes.put(cssProperty, colorToHex(color));
542: return attributes;
543: }
544:
545: public static String getAttribute(Color color) {
546: if (color != null)
547: return colorToHex(color);
548: return null;
549: }
550:
551: class CssParser implements CSSParser.CSSParserCallback {
552: private List<String[]> selectors = new LinkedList<String[]>();
553: private List<String> selectorTokens = new LinkedList<String>();
554: /**
555: * Name of the current property.
556: */
557: private String propertyName;
558: private CSSAttributeSet declaration = new CSSAttributeSet();
559: /** True if parsing a declaration, that is the Reader will not
560: * contain a selector. */
561: // boolean parsingDeclaration;
562: /** True if the attributes are coming from a linked/imported style. */
563: // boolean isLink;
564: /**
565: * Where the CSS stylesheet lives.
566: */
567: private URL base;
568: private CSSParser parser = new CSSParser();
569:
570: /**
571: * Parses the passed in CSS declaration into an CSSPropertySet.
572: */
573: public CSSAttributeSet parseDeclaration(String string) {
574: try {
575: return parseDeclaration(new StringReader(string));
576: } catch (IOException ioe) {
577: }
578: return null;
579: }
580:
581: /**
582: * Parses the passed in CSS declaration into an CSSPropertySet.
583: */
584: public CSSAttributeSet parseDeclaration(Reader r)
585: throws IOException {
586: parse(base, r, true, false);
587: return new CSSAttributeSet(declaration);
588: }
589:
590: /**
591: * Parse the given CSS stream
592: */
593: public void parse(URL base, Reader r, boolean parseDeclaration,
594: boolean isLink) throws IOException {
595: this .base = base;
596: // this.isLink = isLink;
597: // this.parsingDeclaration = parseDeclaration;
598: declaration.clear();
599: selectorTokens.clear();
600: selectors.clear();
601: propertyName = null;
602: parser.parse(r, this , parseDeclaration);
603: }
604:
605: //
606: // CSSParserCallback methods, public to implement the interface.
607: //
608:
609: /**
610: * Invoked when a valid @import is encountered, will call
611: * <code>importStyleSheet</code> if a
612: * <code>MalformedURLException</code> is not thrown in creating
613: * the URL.
614: */
615: public void handleImport(String importString) {
616: URL url = CSSStyleSheet.getURL(base, importString);
617: if (url != null) {
618: importStyleSheet(url);
619: }
620: }
621:
622: /**
623: * A selector has been encountered.
624: */
625: public void handleSelector(String selector) {
626: selector = selector.toLowerCase();
627:
628: int length = selector.length();
629:
630: if (selector.endsWith(",")) {
631: if (length > 1) {
632: selector = selector.substring(0, length - 1);
633: selectorTokens.add(selector);
634: }
635: addSelector();
636: } else if (length > 0) {
637: selectorTokens.add(selector);
638: }
639: }
640:
641: /**
642: * Invoked when the start of a rule is encountered.
643: */
644: public void startRule() {
645: if (selectorTokens.size() > 0) {
646: addSelector();
647: }
648: propertyName = null;
649: }
650:
651: /**
652: * Invoked when a property name is encountered.
653: */
654: public void handleProperty(String property) {
655: propertyName = property;
656: }
657:
658: /**
659: * Invoked when a property value is encountered.
660: */
661: public void handleValue(String value) {
662: if (propertyName != null) {
663: declaration.put(new CSSProperty(propertyName), value);
664: }
665: propertyName = null;
666: }
667:
668: /**
669: * Invoked when the end of a rule is encountered.
670: */
671: public void endRule() {
672: int n = selectors.size();
673: for (int i = 0; i < n; i++) {
674: String[] selector = (String[]) selectors.get(i);
675: for (int j = selector.length - 1; j >= 0; --j) {
676: CSSStyleSheet.this .putStyle(new CSSStyle(
677: new Selector(selector[j]), declaration));
678: }
679: }
680: declaration.clear();
681: selectors.clear();
682: }
683:
684: private void addSelector() {
685: String[] selector = new String[selectorTokens.size()];
686: selector = (String[]) selectorTokens.toArray(selector);
687: selectors.add(selector);
688: selectorTokens.clear();
689: }
690: }
691: }
|