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}