001package com.hfg.css;
002
003import java.awt.Color;
004import java.io.IOException;
005import java.io.Reader;
006import java.util.*;
007
008import com.hfg.graphics.ColorUtil;
009import com.hfg.html.HTMLTag;
010import com.hfg.html.attribute.Align;
011import com.hfg.html.attribute.HTMLColor;
012import com.hfg.html.attribute.VAlign;
013import com.hfg.util.*;
014import com.hfg.util.collection.CollectionUtil;
015
016//------------------------------------------------------------------------------
017/**
018 Class to define some css style shortcuts.
019
020 Use example:
021 <pre>
022 span.setStyle(CSS.BOLD + CSS.color(Color.red) + CSS.ITALIC);
023 </pre>
024 <div>
025  @author J. Alex Taylor, hairyfatguy.com
026 </div>
027 */
028//------------------------------------------------------------------------------
029// com.hfg XML/HTML Coding Library
030//
031// This library is free software; you can redistribute it and/or
032// modify it under the terms of the GNU Lesser General Public
033// License as published by the Free Software Foundation; either
034// version 2.1 of the License, or (at your option) any later version.
035//
036// This library is distributed in the hope that it will be useful,
037// but WITHOUT ANY WARRANTY; without even the implied warranty of
038// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
039// Lesser General Public License for more details.
040//
041// You should have received a copy of the GNU Lesser General Public
042// License along with this library; if not, write to the Free Software
043// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
044//
045// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
046// jataylor@hairyfatguy.com
047//------------------------------------------------------------------------------
048
049public class CSS
050{
051   //###########################################################################
052   // PUBLIC FIELDS
053   //###########################################################################
054
055   public static final String STYLE        = "style";
056
057   /**
058    Shortcut for 'font-weight:bold;'
059    */
060   public static final String BOLD         = "font-weight:bold;";
061   /**
062    Shortcut for 'font-style:italic;'
063    */
064   public static final String ITALIC       = "font-style:italic;";
065   /**
066    Shortcut for 'text-decoration:underline;'
067    */
068   public static final String UNDERLINE    = "text-decoration:underline;";
069   /**
070    Shortcut for 'font-size:small;'
071    */
072   public static final String SMALL_FONT   = "font-size:small;";
073   /**
074    Shortcut for 'font-family:monospace;'
075    */
076   public static final String MONOSPACE    = "font-family:monospace;";
077   /**
078    Shortcut for 'vertical-align:super;'
079    */
080   public static final String SUPER        = "vertical-align:super;";
081
082   //###########################################################################
083   // PRIVATE FIELDS
084   //###########################################################################
085
086   private List<CSSRule> mCSSRules;
087
088   //###########################################################################
089   // CONSTRUCTORS
090   //###########################################################################
091
092   //--------------------------------------------------------------------------
093   public CSS()
094   {
095   }
096
097   //--------------------------------------------------------------------------
098   /**
099    Parses CSS text into rules.
100    @param inReader CSS text
101    @throws IOException if a problem is encountered while processing the CSS text from the specified Reader
102    */
103   public CSS(Reader inReader)
104         throws IOException
105   {
106      mCSSRules = parse(inReader);
107   }
108
109   //###########################################################################
110   // PUBLIC METHODS
111   //###########################################################################
112
113   //--------------------------------------------------------------------------
114   @Override
115   public String toString()
116   {
117      StringBuilderPlus buffer = new StringBuilderPlus();
118      if (CollectionUtil.hasValues(getCSSRules()))
119      {
120         for (CSSRule rule : getCSSRules())
121         {
122            buffer.appendln(rule.toString());
123         }
124      }
125
126      return buffer.toString();
127   }
128
129   //--------------------------------------------------------------------------
130   public CSSRule addRule()
131   {
132      CSSRule rule = new CSSRule();
133      addRule(rule);
134
135      return rule;
136   }
137
138   //--------------------------------------------------------------------------
139   public CSS addRule(CSSRule inRule)
140   {
141      if (null == mCSSRules)
142      {
143         mCSSRules = new ArrayList<>();
144      }
145
146      mCSSRules.add(inRule);
147
148      return this;
149   }
150
151   //--------------------------------------------------------------------------
152   public List<CSSRule> getCSSRules()
153   {
154      return mCSSRules;
155   }
156
157   //--------------------------------------------------------------------------
158   /**
159    Returns CSS declarations that apply to the specified HTML tag.
160    Useful for fusing HTML and CSS before conversion (to WordprocessingML for example).
161    NOTE: This initial implementation does not handle complex selectors.
162    @param inHTML  top tag in a chunk of HTML DOM
163    @param inMediaType CSS media type
164    @return List of CSS declarations
165    */
166   //TODO: improve selector matching
167   public List<CSSDeclaration> getCSSDeclarationsForHTMLTag(HTMLTag inHTML, CSSMediaType inMediaType)
168   {
169      List<CSSDeclaration> declarations = new ArrayList<>(5);
170
171      for (CSSRule cssRule : mCSSRules)
172      {
173         // Does this CSS rule apply to this tag?
174         if (cssRule.appliesTo(inMediaType))
175         {
176            for (CSSSelector selector : cssRule.getSelectors())
177            {
178               if (selector.appliesTo(inHTML))
179               {
180                  if (CollectionUtil.hasValues(cssRule.getDeclarations()))
181                  {
182                     declarations.addAll(cssRule.getDeclarations());
183                  }
184               }
185            }
186         }
187      }
188
189      return declarations;
190   }
191
192   //--------------------------------------------------------------------------
193   /**
194    Where specified CSS rules apply, the styling is placed in the tag's style attribute.
195    Useful for fusing HTML and CSS before conversion (to WordprocessingML for example).
196    NOTE: This initial implementation does not handle complex selectors.
197    @param inHTML  top tag in a chunk of HTML DOM
198    @param inMediaType CSS media type
199    */
200   //TODO: improve selector matching
201   public void localizeStyles(HTMLTag inHTML, CSSMediaType inMediaType)
202   {
203      for (CSSRule cssRule : mCSSRules)
204      {
205         // Does this CSS rule apply to this tag?
206         if (cssRule.appliesTo(inMediaType))
207         {
208            for (CSSSelector selector : cssRule.getSelectors())
209            {
210               if (selector.appliesTo(inHTML))
211               {
212                  // Since the local style css should override the stylesheet css we need to (re)add it last.
213                  String tagStyle = inHTML.getStyle();
214                  inHTML.setStyle(cssRule.getDeclarationString());
215                  if (StringUtil.isSet(tagStyle))
216                  {
217                     inHTML.addStyle(tagStyle);
218                  }
219               }
220            }
221         }
222      }
223
224      // Recurse thru subtags
225      List<HTMLTag> subtags = inHTML.getSubtags();
226      if (CollectionUtil.hasValues(subtags))
227      {
228         for (HTMLTag subtag : subtags)
229         {
230            localizeStyles(subtag, inMediaType);
231         }
232      }
233   }
234
235   //--------------------------------------------------------------------------
236   public static String color(String inColor)
237   {
238      return CSSProperty.color + ":" + (HexUtil.isHex(inColor) && ! inColor.startsWith("#") ? "#" : "") + inColor + ";";
239   }
240
241   //--------------------------------------------------------------------------
242   public static String color(Color inColor)
243   {
244      return CSSProperty.color + ":" + CssUtil.colorToCssValue(inColor) + ";";
245   }
246
247
248   //--------------------------------------------------------------------------
249   public static String bgColor(String inColor)
250   {
251      return CSSProperty.background_color + ":" + (HexUtil.isHex(inColor) && ! inColor.startsWith("#") ? "#" : "") + inColor + ";";
252   }
253
254   //--------------------------------------------------------------------------
255   public static String bgColor(Color inColor)
256   {
257      return CSSProperty.background_color + ":" + CssUtil.colorToCssValue(inColor) + ";";
258   }
259
260
261   //--------------------------------------------------------------------------
262   public static String fontSize(int inPtSize)
263   {
264      return CSSProperty.font_size + ":" + inPtSize + "pt;";
265   }
266
267   //--------------------------------------------------------------------------
268   public static String fontSizeEm(int inValue)
269   {
270      return CSSProperty.font_size + ":" + inValue + "em;";
271   }
272
273   //--------------------------------------------------------------------------
274   public static String width(String inValue)
275   {
276      return CSSProperty.width + ":" + inValue + ";";
277   }
278
279   //--------------------------------------------------------------------------
280   public static String height(String inValue)
281   {
282      return CSSProperty.height + ":" + inValue + ";";
283   }
284
285   //--------------------------------------------------------------------------
286   public static String textAlign(Align inValue)
287   {
288      return CSSProperty.text_align + ":" + inValue + ";";
289   }
290
291   //--------------------------------------------------------------------------
292   public static String verticalAlign(VAlign inValue)
293   {
294      return CSSProperty.vertical_align + ":" + inValue + ";";
295   }
296
297   //--------------------------------------------------------------------------
298   public static String display(CSSDisplayValue inValue)
299   {
300      return CSSProperty.display + ":" + inValue.getDisplayString() + ";";
301   }
302
303   //###########################################################################
304   // PRIVATE METHODS
305   //###########################################################################
306
307   //--------------------------------------------------------------------------
308   private List<CSSRule> parse(Reader inReader)
309         throws IOException
310   {
311      List<CSSRule> cssRules = new ArrayList<>();
312
313      CSSRule currentCSSRule = null;
314      Set<CSSMediaQuery> currentMediaQueries = null;
315      boolean parsingMediaQueryList = false;
316      boolean parsingSelector    = true;
317      boolean parsingDeclaration = false;
318      boolean inBlockComment     = false;
319
320      StringBuilder mediaQueryListBuffer = new StringBuilder();
321      StringBuilder selectorBuffer    = new StringBuilder();
322      StringBuilder declarationBuffer = new StringBuilder();
323      Character prevChar = null;
324
325      int currentChar;
326      while ((currentChar = inReader.read()) != -1)
327      {
328         if (inBlockComment)
329         {
330            if (currentChar == '/'
331                && prevChar == '*')
332            {
333               inBlockComment = false;
334            }
335         }
336         else if (parsingMediaQueryList)
337         {
338            if (currentChar == '{')
339            {
340               parsingSelector = true;
341               parsingMediaQueryList = false;
342
343               String[] mediaQueries = mediaQueryListBuffer.toString().split(",");
344               currentMediaQueries = new HashSet<CSSMediaQuery>(mediaQueries.length);
345               for (String mediaQueryString : mediaQueries)
346               {
347                  currentMediaQueries.add(new CSSMediaQuery(mediaQueryString));
348               }
349            }
350            else
351            {
352               mediaQueryListBuffer.append((char)currentChar);
353            }
354         }
355         else if (parsingSelector
356                  && currentChar == 'a'
357                  && selectorBuffer.toString().trim().equals("@medi"))
358         {
359            parsingSelector = false;
360            parsingMediaQueryList = true;
361            selectorBuffer.setLength(0);
362         }
363         else if (currentChar == '*'
364             && prevChar == '/')
365         {
366            inBlockComment = true;
367            // Back out the slash from the buffer
368            if (parsingSelector)
369            {
370               selectorBuffer.setLength(selectorBuffer.length() - 1);
371            }
372            else if (parsingDeclaration)
373            {
374               declarationBuffer.setLength(declarationBuffer.length() - 1);
375            }
376         }
377         else if (parsingSelector)
378         {
379            if (currentChar == ','
380                || currentChar == '{')
381            {
382               if (null == currentCSSRule)
383               {
384                  currentCSSRule = new CSSRule().setMediaQueries(currentMediaQueries);
385               }
386
387               currentCSSRule.addSelector(new CSSSelector(selectorBuffer.toString().trim()));
388               selectorBuffer.setLength(0);
389
390               if (currentChar == '{')
391               {
392                  parsingSelector = false;
393                  parsingDeclaration = true;
394               }
395            }
396            else if (currentChar == '}'
397                     && CollectionUtil.hasValues(currentMediaQueries))
398            {
399               currentMediaQueries = null;
400               mediaQueryListBuffer.setLength(0);
401               selectorBuffer.setLength(0);
402            }
403            else
404            {
405               selectorBuffer.append((char)currentChar);
406            }
407         }
408         else if (parsingDeclaration)
409         {
410            if (currentChar == ';'
411                  || currentChar == '}')
412            {
413               String declaration = declarationBuffer.toString().trim();
414               if (StringUtil.isSet(declaration))
415               {
416                  String[] pieces = declaration.split(":");
417                  if (2 == pieces.length)
418                  {
419                     CSSProperty cssProperty = CSSProperty.valueOf(pieces[0]);
420                     currentCSSRule.addDeclaration(new CSSDeclaration(cssProperty, pieces[1]));
421                  }
422                  declarationBuffer.setLength(0);
423               }
424
425               if (currentChar == '}')
426               {
427                  parsingSelector = true;
428                  parsingDeclaration = false;
429                  cssRules.add(currentCSSRule);
430                  currentCSSRule = null;
431               }
432            }
433            else
434            {
435               declarationBuffer.append((char)currentChar);
436            }
437         }
438
439         prevChar = (char) currentChar;
440      }
441
442      return cssRules;
443   }
444
445}