001package com.hfg.css;
002
003
004import java.util.Arrays;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Set;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010
011import com.hfg.html.HTMLTag;
012import com.hfg.util.collection.CollectionUtil;
013import com.hfg.util.StringUtil;
014import com.hfg.xml.XMLContainer;
015
016//------------------------------------------------------------------------------
017/**
018 CSS selector encapsulation.
019 See <a href='http://www.w3.org/TR/CSS2/selector.html'>http://www.w3.org/TR/CSS2/selector.html</a>
020 <div>
021  @author J. Alex Taylor, hairyfatguy.com
022 </div>
023 */
024//------------------------------------------------------------------------------
025// com.hfg XML/HTML Coding Library
026//
027// This library is free software; you can redistribute it and/or
028// modify it under the terms of the GNU Lesser General Public
029// License as published by the Free Software Foundation; either
030// version 2.1 of the License, or (at your option) any later version.
031//
032// This library is distributed in the hope that it will be useful,
033// but WITHOUT ANY WARRANTY; without even the implied warranty of
034// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
035// Lesser General Public License for more details.
036//
037// You should have received a copy of the GNU Lesser General Public
038// License along with this library; if not, write to the Free Software
039// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
040//
041// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
042// jataylor@hairyfatguy.com
043//------------------------------------------------------------------------------
044
045
046public class CSSSelector
047{
048   private String mStringValue;
049   private String mBaseTag;
050   private String mId;
051   private CSSSelector mAncestorSelector;
052   private CSSSelector mParentSelector;
053   private CSSSelector mPrecedingSiblingSelector;
054   private Set<String> mClassNames;
055   private Set<AttributeCriteria> mAttributeCriteria;
056
057   private Pattern sAttrPattern = Pattern.compile("(\\w+)(?:([\\~\\|]?\\=)(.+))?");
058
059   private static enum AttributeCriteriaFlag { CONTAINS, PREFIX }
060
061   //##########################################################################
062   // CONSTRUCTORS
063   //##########################################################################
064
065   //--------------------------------------------------------------------------
066   public CSSSelector(String inValue)
067   {
068      mStringValue = inValue;
069
070      init();
071   }
072
073   //##########################################################################
074   // PUBLIC METHODS
075   //##########################################################################
076
077   //--------------------------------------------------------------------------
078   /**
079    Currently supports simple type selectors, id selectors, class selectors,
080    attribute selectors, ancestor selectors, parent selectors, and adjacent sibling selectors.
081    @param inHTMLTag the HTML tag to be checked
082    @return boolean whether or not the CSS selector applies to the specified HTML tag
083    */
084   // TODO: Support for pseudo-classes, pseudo-elements
085   public boolean appliesTo(HTMLTag inHTMLTag)
086   {
087      boolean applies = false;
088
089      if (null == mBaseTag
090          || inHTMLTag.getTagName().equalsIgnoreCase(mBaseTag))
091      {
092         applies = true;
093
094         if (mId != null)
095         {
096            applies = mId.equals(inHTMLTag.getId());
097         }
098
099         if (CollectionUtil.hasValues(mClassNames))
100         {
101            applies = false;
102
103            String tagClassString = inHTMLTag.getClassAttribute();
104            if (StringUtil.isSet(tagClassString))
105            {
106               List<String> tagClassNames = Arrays.asList(tagClassString.split("\\s+"));
107               applies = true;
108               for (String className : mClassNames)
109               {
110                  if (! tagClassNames.contains(className))
111                  {
112                     applies = false;
113                     break;
114                  }
115               }
116            }
117         }
118
119         if (applies
120             && CollectionUtil.hasValues(mAttributeCriteria))
121         {
122            for (AttributeCriteria attrCriteria : mAttributeCriteria)
123            {
124               if (! attrCriteria.appliesTo(inHTMLTag))
125               {
126                  applies = false;
127                  break;
128               }
129            }
130         }
131
132
133         if (applies)
134         {
135            if (mAncestorSelector != null)
136            {
137               applies = false;
138
139               XMLContainer parentTag = inHTMLTag.getParentNode();
140               while (! applies
141                     && parentTag != null)
142               {
143                  if (parentTag instanceof HTMLTag
144                        && mAncestorSelector.appliesTo((HTMLTag)parentTag))
145                  {
146                     applies = true;
147                  }
148
149                  parentTag = parentTag.getParentNode();
150               }
151            }
152            else if (mParentSelector != null)
153            {
154               XMLContainer parentTag = inHTMLTag.getParentNode();
155
156               applies = (parentTag instanceof HTMLTag
157                          && mParentSelector.appliesTo((HTMLTag) parentTag));
158            }
159            else if (mPrecedingSiblingSelector != null)
160            {
161               XMLContainer previousSibling = inHTMLTag.getPreviousSibling();
162
163               applies = (previousSibling instanceof HTMLTag
164                          && mPrecedingSiblingSelector.appliesTo((HTMLTag) previousSibling));
165            }
166         }
167      }
168
169      return applies;
170   }
171
172   //--------------------------------------------------------------------------
173   @Override
174   public String toString()
175   {
176      return mStringValue;
177   }
178
179   //--------------------------------------------------------------------------
180   @Override
181   public int hashCode()
182   {
183      return mStringValue.hashCode();
184   }
185
186   //--------------------------------------------------------------------------
187   @Override
188   public boolean equals(Object inObj)
189   {
190      return (inObj != null
191              && inObj instanceof CSSSelector
192              && inObj.toString().equals(toString()));
193   }
194
195   //##########################################################################
196   // PRIVATE METHODS
197   //##########################################################################
198
199   //--------------------------------------------------------------------------
200   private void init()
201   {
202      String[] pieces = mStringValue.split("\\s+");
203      String finalPiece = pieces[pieces.length - 1];
204      if (2 == pieces.length)
205      {
206         mAncestorSelector =  new CSSSelector(pieces[0]);
207      }
208      else if (3 == pieces.length
209               && pieces[1].equals(">"))
210      {
211         mParentSelector = new CSSSelector(pieces[0]);
212      }
213      else if (3 == pieces.length
214               && pieces[1].equals("+"))
215      {
216         mPrecedingSiblingSelector = new CSSSelector(pieces[0]);
217      }
218
219      parse(finalPiece);
220   }
221
222   //--------------------------------------------------------------------------
223   private void parse(String inSelectorString)
224   {
225      char[] chars = inSelectorString.toCharArray();
226      boolean parsingBaseTag   = true;
227      boolean parsingClassName = false;
228      boolean parsingId        = false;
229      boolean parsingAttribute = false;
230      int startIndex = 0;
231      for (int i = 0; i < chars.length; i++)
232      {
233         char currentChar = chars[i];
234         if (parsingBaseTag)
235         {
236            if (currentChar == '.')
237            {
238               parsingBaseTag = false;
239               parsingClassName = true;
240            }
241            else if (currentChar == '#')
242            {
243               parsingBaseTag = false;
244               parsingId = true;
245            }
246            else if (currentChar == '[')
247            {
248               parsingBaseTag = false;
249               parsingAttribute = true;
250            }
251
252            if (! parsingBaseTag)
253            {
254               if (i > 0)
255               {
256                  mBaseTag = inSelectorString.substring(0, i);
257               }
258
259               startIndex = i + 1;
260            }
261         }
262         else if (parsingClassName)
263         {
264            if (currentChar == '.')
265            {
266               addClassName(inSelectorString.substring(startIndex, i));
267
268               startIndex = i + 1;
269            }
270            else if (currentChar == '#')
271            {
272               parsingClassName = false;
273               parsingId = true;
274            }
275            else if (currentChar == '[')
276            {
277               parsingClassName = false;
278               parsingAttribute = true;
279            }
280
281            if (! parsingClassName)
282            {
283               addClassName(inSelectorString.substring(startIndex, i));
284
285               startIndex = i + 1;
286            }
287         }
288         else if (parsingId)
289         {
290            if (currentChar == '.')
291            {
292               parsingId = false;
293               parsingClassName = true;
294            }
295            else if (currentChar == '[')
296            {
297               parsingId = false;
298               parsingAttribute = true;
299            }
300
301            if (! parsingId)
302            {
303               mId = inSelectorString.substring(startIndex, i);
304
305               startIndex = i + 1;
306            }
307         }
308         else if (parsingAttribute)
309         {
310            if (currentChar == ']')
311            {
312               addAttributeCriteria(inSelectorString.substring(startIndex, i));
313               parsingAttribute = false;
314            }
315         }
316         else if (currentChar == '[')
317         {
318            parsingAttribute = true;
319            startIndex = i + 1;
320         }
321      }
322
323      // Anything left unparsed when we hit the end of the string?
324      if (parsingBaseTag)
325      {
326         mBaseTag = inSelectorString;
327      }
328      else if (parsingClassName)
329      {
330         addClassName(inSelectorString.substring(startIndex));
331      }
332      else if (parsingId)
333      {
334         mId = inSelectorString.substring(startIndex);
335      }
336      else if (parsingAttribute)
337      {
338         throw new CSSException("Problem parsing CSS selector " + StringUtil.singleQuote(inSelectorString) + "!");
339      }
340
341
342      if ("*".equals(mBaseTag))
343      {
344         mBaseTag = null;
345      }
346   }
347
348   //--------------------------------------------------------------------------
349   private void addClassName(String inValue)
350   {
351      if (null == mClassNames)
352      {
353         mClassNames = new HashSet<String>(4);
354      }
355
356      mClassNames.add(inValue);
357   }
358
359   //--------------------------------------------------------------------------
360   private void addAttributeCriteria(String inValue)
361   {
362      if (null == mAttributeCriteria)
363      {
364         mAttributeCriteria = new HashSet<AttributeCriteria>(2);
365      }
366
367      Matcher m = sAttrPattern.matcher(inValue);
368      if (m.matches())
369      {
370         AttributeCriteria criteria = new AttributeCriteria(m.group(1), m.group(3));
371         if (m.group(2) != null)
372         {
373            if (m.group(2).equals("~="))
374            {
375               criteria.setFlag(AttributeCriteriaFlag.CONTAINS);
376            }
377            else if (m.group(2).equals("|="))
378            {
379               criteria.setFlag(AttributeCriteriaFlag.PREFIX);
380            }
381         }
382
383         mAttributeCriteria.add(criteria);
384      }
385      else
386      {
387         throw new CSSException("Problem parsing attribute selector " + StringUtil.singleQuote(inValue) + "!");
388      }
389
390   }
391
392   //##########################################################################
393   // INNER CLASS
394   //##########################################################################
395
396   private class AttributeCriteria
397   {
398      private String mAttr;
399      private String mValue;
400      private AttributeCriteriaFlag mFlag;
401
402      //-----------------------------------------------------------------------
403      public AttributeCriteria(String inName, String inValue)
404      {
405         mAttr  = inName;
406         mValue = StringUtil.isSet(inValue) ? StringUtil.unquote(inValue) : null;
407      }
408
409      //-----------------------------------------------------------------------
410      public AttributeCriteria setFlag(AttributeCriteriaFlag inValue)
411      {
412         mFlag = inValue;
413         return this;
414      }
415
416      //-----------------------------------------------------------------------
417      public boolean appliesTo(HTMLTag inHTMLTag)
418      {
419         boolean applies = false;
420
421         String attrValue = inHTMLTag.getAttributeValue(mAttr);
422         if (attrValue != null)
423         {
424            applies = true;
425
426            if (mValue != null)
427            {
428               if (AttributeCriteriaFlag.CONTAINS == mFlag)
429               {
430                  applies = false;
431                  String[] pieces = attrValue.split("\\s+");
432                  for (String piece : pieces)
433                  {
434                     if (piece.equals(mValue))
435                     {
436                        applies = true;
437                        break;
438                     }
439                  }
440               }
441               else if (AttributeCriteriaFlag.PREFIX == mFlag)
442               {
443                  applies = mValue.equals(attrValue) || attrValue.startsWith(mValue + "-");
444               }
445               else
446               {
447                  applies = mValue.equals(attrValue);
448               }
449            }
450         }
451
452         return applies;
453      }
454   }
455}