Package freenet.client.filter

Source Code of freenet.client.filter.CSSTokenizerFilter$ParsedCounter

/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */

package freenet.client.filter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import freenet.support.Fields;
import freenet.support.Logger;
import freenet.support.api.Bucket;
import freenet.support.io.Closer;
import freenet.support.io.FileBucket;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;

/** Comprehensive CSS2.1 filter. The old jflex-based filter was very far
* from comprehensive.
* @author kurmiashish
* @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
*
* FIXME: Rewrite to parse properly. This works but is rather spaghettified.
* JFlex on its own obviously won't work, but JFlex plus a proper grammar
* should work fine.
*
* According to the CSS2.1 spec, in some cases spaces between tokens are
* optional. We do NOT support this! A grammar-based parser might, although
* really it's horrible, we shouldn't support it if it's not widely used...
* See end of 1.4.2.1.
*/
class CSSTokenizerFilter {
  private Reader r;
  Writer w = null;
  FilterCallback cb;
  private static volatile boolean logDEBUG;
  private final String passedCharset;
  private String detectedCharset;
  private final boolean stopAtDetectedCharset;
  private final boolean isInline;

  static {
    Logger.registerClass(CSSTokenizerFilter.class);
  }

  CSSTokenizerFilter(){
    passedCharset = "UTF-8";
    stopAtDetectedCharset = false;
    isInline = false;
  }
       
  CSSTokenizerFilter(Reader r, Writer w, FilterCallback cb, String charset, boolean stopAtDetectedCharset, boolean isInline) {
    this.r=r;
    this.w = w;
    this.cb=cb;
    passedCharset = charset;
    this.stopAtDetectedCharset = stopAtDetectedCharset;
    this.isInline = isInline;
  }

  public boolean isValidURI(String URI)
  {
    try
    {
      return URI.equals(cb.processURI(URI, null));
    }
    catch(CommentException e)
    {
      return false;
    }
  }


  //Function to merge two arrays into third array.
  public static <T> T[] concat(T[] a, T[] b) {
    final int alen = a.length;
    final int blen = b.length;
    if (alen == 0) {
      return b;
    }
    if (blen == 0) {
      return a;
    }
    @SuppressWarnings("unchecked") final T[] result = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), alen + blen);
    System.arraycopy(a, 0, result, 0, alen);
    System.arraycopy(b, 0, result, alen, blen);
    return result;
  }


  /* To save the memory, only those Verifier objects would be created which are actually present in the CSS document.
   * allelementVerifiers contains all the CSS property tags as String. All loaded Verifier objects are stored in elementVerifier.
   * When retrieving a Verifier object, first it is searched in elementVerifiers to see if it is already loaded.
   * If it is not loaded then allelementVerifiers is checked to see if the property name is valid. If it is valid, then the desired Verifier object is loaded in allelemntVerifiers.
   */

  // FIXME this is probably overkill, initialising all of them on startup would probably be cleaner code, less synchronization, at very little memory cost.
  // FIXME check how many bytes we save by lazy init here.

  private final static Map<String, CSSPropertyVerifier> elementVerifiers = new HashMap<String, CSSPropertyVerifier>();
  private final static HashSet<String> allelementVerifiers=new HashSet<String>();
  //Reference http://www.w3.org/TR/CSS2/propidx.html
  static {
    allelementVerifiers.add("azimuth");
    allelementVerifiers.add("background-attachment");
    allelementVerifiers.add("background-clip");
    allelementVerifiers.add("background-color");
    allelementVerifiers.add("background-image");
    allelementVerifiers.add("background-origin");
    allelementVerifiers.add("background-position");
    allelementVerifiers.add("background-repeat");
    allelementVerifiers.add("background-size");
    allelementVerifiers.add("background");
    allelementVerifiers.add("border-collapse");
    allelementVerifiers.add("border-color");
    allelementVerifiers.add("border-top-color");
    allelementVerifiers.add("border-bottom-color");
    allelementVerifiers.add("border-right-color");
    allelementVerifiers.add("border-left-color");
    allelementVerifiers.add("border-spacing");
    allelementVerifiers.add("border-style");
    allelementVerifiers.add("border-top-style");
    allelementVerifiers.add("border-bottom-style");
    allelementVerifiers.add("border-left-style");
    allelementVerifiers.add("border-right-style");
    allelementVerifiers.add("border-left");
    allelementVerifiers.add("border-top");
    allelementVerifiers.add("border-right");
    allelementVerifiers.add("border-bottom");
    allelementVerifiers.add("border-top-color");
    allelementVerifiers.add("border-right-color");
    allelementVerifiers.add("border-bottom-color");
    allelementVerifiers.add("border-left-color");
    allelementVerifiers.add("border-top-style");
    allelementVerifiers.add("border-right-style");
    allelementVerifiers.add("border-bottom-style");
    allelementVerifiers.add("border-top-width");
    allelementVerifiers.add("border-right-width");
    allelementVerifiers.add("border-bottom-width");
    allelementVerifiers.add("border-left-width");
    allelementVerifiers.add("border-width");
    allelementVerifiers.add("border-top-width");
    allelementVerifiers.add("border-bottom-width");
    allelementVerifiers.add("border-left-width");
    allelementVerifiers.add("border-right-width");
    allelementVerifiers.add("border-radius");
    allelementVerifiers.add("border-top-radius");
    allelementVerifiers.add("border-bottom-radius");
    allelementVerifiers.add("border-left-radius");
    allelementVerifiers.add("border-right-radius");
    allelementVerifiers.add("border-image-source");
    allelementVerifiers.add("border-image-slice");
    allelementVerifiers.add("border-image-width");
    allelementVerifiers.add("border-image-outset");
    allelementVerifiers.add("border-image-repeat");
    allelementVerifiers.add("border-image");
    allelementVerifiers.add("border");
    allelementVerifiers.add("bottom");
    allelementVerifiers.add("box-decoration-break");
    allelementVerifiers.add("box-shadow");
    allelementVerifiers.add("caption-side");
    allelementVerifiers.add("clear");
    allelementVerifiers.add("clip");
                allelementVerifiers.add("break-before");
                allelementVerifiers.add("break-after");
                allelementVerifiers.add("break-inside");
                allelementVerifiers.add("column-count");
                allelementVerifiers.add("column-fill");
                allelementVerifiers.add("column-gap");
                allelementVerifiers.add("column-rule-color");
                allelementVerifiers.add("column-rule-style");
                allelementVerifiers.add("column-rule-width");
                allelementVerifiers.add("column-span");
    allelementVerifiers.add("column-rule");
                allelementVerifiers.add("column-width");
    allelementVerifiers.add("columns");
    allelementVerifiers.add("color");
    allelementVerifiers.add("color-interpolation");
    allelementVerifiers.add("color-rendering");
    allelementVerifiers.add("content");
    allelementVerifiers.add("counter-increment");
    allelementVerifiers.add("counter-reset");
    allelementVerifiers.add("cue-after");
    allelementVerifiers.add("cue-before");
    allelementVerifiers.add("cue");
    allelementVerifiers.add("cursor");
    allelementVerifiers.add("direction");
    allelementVerifiers.add("display");
    allelementVerifiers.add("elevation");
    allelementVerifiers.add("empty-cells");
    allelementVerifiers.add("float");
    allelementVerifiers.add("font-family");
    allelementVerifiers.add("font-size");
    allelementVerifiers.add("font-style");
    allelementVerifiers.add("font-variant");
    allelementVerifiers.add("font-weight");
    allelementVerifiers.add("font");
    allelementVerifiers.add("hanging-punctuation");
    allelementVerifiers.add("height");
    allelementVerifiers.add("left");
    allelementVerifiers.add("letter-spacing");
    allelementVerifiers.add("line-break");
    allelementVerifiers.add("line-height");
    allelementVerifiers.add("list-style-image");
    allelementVerifiers.add("list-style-position");
    allelementVerifiers.add("list-style-type");
    allelementVerifiers.add("list-style");
    allelementVerifiers.add("margin-right");
    allelementVerifiers.add("margin-left");
    allelementVerifiers.add("margin-top");
    allelementVerifiers.add("margin-bottom");
    allelementVerifiers.add("margin");
    allelementVerifiers.add("max-height");
    allelementVerifiers.add("max-width");
    allelementVerifiers.add("min-height");
    allelementVerifiers.add("min-width");
    allelementVerifiers.add("opacity");
    allelementVerifiers.add("orphans");
    allelementVerifiers.add("outline-color");
    allelementVerifiers.add("outline-style");
    allelementVerifiers.add("outline-width");
    allelementVerifiers.add("outline");
    allelementVerifiers.add("overflow");
    allelementVerifiers.add("padding-top");
    allelementVerifiers.add("padding-right");
    allelementVerifiers.add("padding-bottom");
    allelementVerifiers.add("padding-left");
    allelementVerifiers.add("padding");
    allelementVerifiers.add("page-break-after");
    allelementVerifiers.add("page-break-before");
    allelementVerifiers.add("page-break-inside");
    allelementVerifiers.add("pause-after");
    allelementVerifiers.add("pause-before");
    allelementVerifiers.add("pause");
    allelementVerifiers.add("pitch-range");
    allelementVerifiers.add("pitch");
    allelementVerifiers.add("play-during");
    allelementVerifiers.add("punctuation-trim");
    allelementVerifiers.add("position");
    allelementVerifiers.add("quotes");
    allelementVerifiers.add("richness");
    allelementVerifiers.add("right");
    allelementVerifiers.add("speak-header");
    allelementVerifiers.add("speak-numeral");
    allelementVerifiers.add("speak-punctuation");
    allelementVerifiers.add("speak");
    allelementVerifiers.add("speech-rate");
    allelementVerifiers.add("stress");
    allelementVerifiers.add("table-layout");
    allelementVerifiers.add("text-align");
    allelementVerifiers.add("text-align-last");
    allelementVerifiers.add("text-autospace");
    allelementVerifiers.add("text-decoration");
    allelementVerifiers.add("text-decoration-color");
    allelementVerifiers.add("text-decoration-line");
    allelementVerifiers.add("text-decoration-skip");
    allelementVerifiers.add("text-decoration-style");
    allelementVerifiers.add("text-emphasis");
    allelementVerifiers.add("text-emphasis-color");
    allelementVerifiers.add("text-emphasis-position");
    allelementVerifiers.add("text-emphasis-style");
    allelementVerifiers.add("text-indent");
    allelementVerifiers.add("text-justify");
    allelementVerifiers.add("text-outline");
    allelementVerifiers.add("text-overflow");
    allelementVerifiers.add("text-shadow");
    allelementVerifiers.add("text-transform");
    allelementVerifiers.add("text-underline-position");
    allelementVerifiers.add("text-wrap");
    allelementVerifiers.add("top");
    allelementVerifiers.add("transform");
    allelementVerifiers.add("transform-origin");
    allelementVerifiers.add("unicode-bidi");
    allelementVerifiers.add("vertical-align");
    allelementVerifiers.add("visibility");
    allelementVerifiers.add("voice-family");
    allelementVerifiers.add("volume");
    allelementVerifiers.add("white-space");
    allelementVerifiers.add("white-space-collapsing");
    allelementVerifiers.add("widows");
    allelementVerifiers.add("width");
    allelementVerifiers.add("word-break");
    allelementVerifiers.add("word-spacing");
                allelementVerifiers.add("word-wrap");
    allelementVerifiers.add("z-index");


  }

  /*
   * Array for storing additional Verifier objects for validating Regular expressions in CSS Property value
   * e.g. [ <color> | transparent]{1,4}. It is explained in detail in CSSPropertyVerifier class
   */
  private final static CSSPropertyVerifier[] auxilaryVerifiers=new CSSPropertyVerifier[117];
  static
  {
    /*CSSPropertyVerifier(String[] allowedValues,String[] possibleValues,String expression,boolean onlyValueVerifier)*/
    //for background-position
    auxilaryVerifiers[2]=new CSSPropertyVerifier(Arrays.asList("left","center","right"),Arrays.asList("pe","le"),null,null,true);
    auxilaryVerifiers[3]=new CSSPropertyVerifier(Arrays.asList("top","center","bottom"),Arrays.asList("pe","le"),null,null,true);
    auxilaryVerifiers[4]=new CSSPropertyVerifier(Arrays.asList("left","center","right"),null,null,null,true);
    auxilaryVerifiers[5]=new CSSPropertyVerifier(Arrays.asList("top","center","bottom"),null,null,null,true);
    //<border-color>
    auxilaryVerifiers[11]=new CSSPropertyVerifier(Arrays.asList("transparent"),Arrays.asList("co"),null,null,true);
    //<border-style>
    auxilaryVerifiers[13]=new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),Arrays.asList("le"),null,null,true);
    //<border-width>
    auxilaryVerifiers[14]=new CSSPropertyVerifier(Arrays.asList("thin","medium","thick"),Arrays.asList("le"),null,null,true);
    //<border-top-color>
    auxilaryVerifiers[15]=new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),Arrays.asList("co"),null,null,true);

    // <background-clip> <background-origin>
    auxilaryVerifiers[61]=new CSSPropertyVerifier(Arrays.asList("border-box", "padding-box", "content-box"),null,null,null,true);
    // <border-radius>
    auxilaryVerifiers[64]=new CSSPropertyVerifier(null,Arrays.asList("le", "pe"),null,null,true);

    // <shadow>
    auxilaryVerifiers[71]=new CSSPropertyVerifier(Arrays.asList("inset"), null, null, null, true);
    auxilaryVerifiers[72]=new CSSPropertyVerifier(null, Arrays.asList("le"), null, null, true);
    auxilaryVerifiers[73]=new CSSPropertyVerifier(null, Arrays.asList("co"), null, null, true);
    auxilaryVerifiers[74]=new CSSPropertyVerifier(null, null, Arrays.asList("72<1,4>"), null, true);
    auxilaryVerifiers[75]=new CSSPropertyVerifier(null, null, Arrays.asList("71a74a73"), null, true);

    // <border-image-source>
    auxilaryVerifiers[76]=new CSSPropertyVerifier(Arrays.asList("none"),Arrays.asList("ur"),null,null,true);

    // <border-image-slice>
    auxilaryVerifiers[68]=new CSSPropertyVerifier(Arrays.asList("auto"),Arrays.asList("le","pe","in"),null,null,true);
    auxilaryVerifiers[77]=new CSSPropertyVerifier(null,null,Arrays.asList("68<1,4>"),null,true);

    // <border-image-repeat>
    auxilaryVerifiers[70]=new CSSPropertyVerifier(Arrays.asList("stretch","repeat","round"),null,null,null,true);
    auxilaryVerifiers[78]=new CSSPropertyVerifier(null,null,Arrays.asList("70<1,2>"),null,true);

    // <text-shadow>
    auxilaryVerifiers[79]=new CSSPropertyVerifier(null, null, Arrays.asList("74a73"), null, true);

    // <spacing-limit>
    auxilaryVerifiers[85]=new CSSPropertyVerifier(Arrays.asList("normal"), Arrays.asList("le","pe"), null, null, true);

    // <text-decoration-line>
    auxilaryVerifiers[100] = new CSSPropertyVerifier(Arrays.asList("underline"), null, null, null, true);
    auxilaryVerifiers[101] = new CSSPropertyVerifier(Arrays.asList("overline"), null, null, null, true);
    auxilaryVerifiers[102] = new CSSPropertyVerifier(Arrays.asList("line-through"), null, null, null, true);
    auxilaryVerifiers[115] = new CSSPropertyVerifier(Arrays.asList("none"),null,null,Arrays.asList("100a101a102"));
    auxilaryVerifiers[116] = new CSSPropertyVerifier(Arrays.asList("blink"), null, null, null, true);
    // <text-decoration-color>
    auxilaryVerifiers[103] = new CSSPropertyVerifier(null, Arrays.asList("co"), null, null, true);
    // <text-decoration-style>
    auxilaryVerifiers[104] = new CSSPropertyVerifier(Arrays.asList("solid", "double", "dotted", "dashed", "wave"), null, null, null, true);

    // <text-emphasis-style>
    auxilaryVerifiers[105]=new CSSPropertyVerifier(Arrays.asList("filled","open"),null,null,null,true);
    auxilaryVerifiers[106]=new CSSPropertyVerifier(Arrays.asList("dot","circle","double-circle","triangle","sesame"),null,null,null,true);
    auxilaryVerifiers[107]=new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,Arrays.asList("st"),Arrays.asList("105a106"));
  }
  /* This function loads a verifier object in elementVerifiers.
   * After the object has been loaded, property name is removed from allelementVerifier.
   */
  private static void addVerifier(String element)
  {
    if("azimuth".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[0]=new CSSPropertyVerifier(Arrays.asList("left-side","far-left","left","center-left","center","center-right","right","far-right","right-side"),null,null,null,true);
      auxilaryVerifiers[1]=new CSSPropertyVerifier(Arrays.asList("behind"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("leftwards","rightwards","inherit"),ElementInfo.AURALMEDIA,Arrays.asList("an"),Arrays.asList("0a1")));
      allelementVerifiers.remove(element);
    }
    else if("background-attachment".equalsIgnoreCase(element)){
      auxilaryVerifiers[60] = new CSSPropertyVerifier(Arrays.asList("local","scroll","fixed"), null, null, null, true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("60<1,65535>"), true,true));
      allelementVerifiers.remove(element);
    }
    else if("background-clip".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("61<1,65535>"), true,true));
      allelementVerifiers.remove(element);

    }
    else if("background-color".equalsIgnoreCase(element)){
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);

    }
    else if("background-image".equalsIgnoreCase(element)){
      auxilaryVerifiers[56] = new CSSPropertyVerifier(Arrays.asList("none"),Arrays.asList("ur"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("56<1,65535>"), true,true));
      allelementVerifiers.remove(element);
    }
    else if("background-origin".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("61<1,65535>"), true,true));
      allelementVerifiers.remove(element);

    }
    else if("background-position".equalsIgnoreCase(element))
    {       // FIXME: css3 http://www.w3.org/TR/css3-background/#background-position
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("2 3?","4a5")));
      allelementVerifiers.remove(element);
    }
    else if("background-repeat".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[57] = new CSSPropertyVerifier(Arrays.asList("repeat","space","round","no-repeat"),null,null,null,true);
      auxilaryVerifiers[58] = new CSSPropertyVerifier(Arrays.asList("repeat-x","repeat-y"), null, null, null, true);
      auxilaryVerifiers[59] = new CSSPropertyVerifier(null, null, Arrays.asList("58","57<1,2>"), null, true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("59<1,65535>"), true,true));
      allelementVerifiers.remove(element);
    }
    else if("background-size".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[61] = new CSSPropertyVerifier(Arrays.asList("auto"),Arrays.asList("le", "pe"),null,null,true);
      auxilaryVerifiers[62] = new CSSPropertyVerifier(Arrays.asList("cover", "contain"), null, null, null, true);
      auxilaryVerifiers[63] = new CSSPropertyVerifier(null, null, Arrays.asList("61<1,2>", "62"), null, true);
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("63<1,65535>"), true,true));
      allelementVerifiers.remove(element);
    }
    else if("background".equalsIgnoreCase(element))
    {    // FIXME: CSS3 http://www.w3.org/TR/css3-background/#background
      //background-attachment
      auxilaryVerifiers[6]=new CSSPropertyVerifier(Arrays.asList("scroll","fixed","inherit"),null,null,null,true);
      //background-color
      auxilaryVerifiers[7]=new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),Arrays.asList("co"),null,null,true);
      //background-image
      auxilaryVerifiers[8]=new CSSPropertyVerifier(Arrays.asList("none","inherit"),Arrays.asList("ur"),null,null,true);
      //background-position
      auxilaryVerifiers[9]=new CSSPropertyVerifier(Arrays.asList("inherit"),null,Arrays.asList("2 3?","4a5"),null,true);
      //background-repeat
      auxilaryVerifiers[10]=new CSSPropertyVerifier(Arrays.asList("repeat","repeat-x","repeat-y","no-repeat","inherit"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("6a7a8a9a10")));
      allelementVerifiers.remove(element);
    }
    else if("border-collapse".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("collapse","separate","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);

    }
    else if("border-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co"),Arrays.asList("11<1,4>")));
      allelementVerifiers.remove(element);

    }
    else if("border-top-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("11"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("11"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-left-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("11"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-right-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("11"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-spacing".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[12]=new CSSPropertyVerifier(null,Arrays.asList("le"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("12 12?")));
      allelementVerifiers.remove(element);
    }
    else if("border-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13<1,4>")));
      allelementVerifiers.remove(element);
    }
    else if("border-top-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("13"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("13"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-left-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("13"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-right-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null, null, Arrays.asList("13"), ElementInfo.VISUALMEDIA, true));
      allelementVerifiers.remove(element);
    }
    else if("border-left".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13a14a15")));
      allelementVerifiers.remove(element);
    }
    else if("border-top".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13a14a15")));
      allelementVerifiers.remove(element);
    }
    else if("border-right".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13a14a15")));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13a14a15")));
      allelementVerifiers.remove(element);
    }
    else if("border-top-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);

    }
    else if("border-right-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);
    }
    else if("border-left-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("transparent","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);
    }
    else if("border-top-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("border-right-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("border-left-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("border-top-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("thin","medium","thick","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
    }
    else if("border-right-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("thin","medium","thick","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("thin","medium","thick","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
    }
    else if("border-left-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("thin","medium","thick","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
    }
    else if("border-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("14<1,4>")));
      allelementVerifiers.remove(element);
    }
    else if("border-top-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("14")));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("14")));
      allelementVerifiers.remove(element);
    }
    else if("border-left-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("14")));
      allelementVerifiers.remove(element);
    }
    else if("border-right-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("14")));
      allelementVerifiers.remove(element);
    }
    else if("border".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13a14a15")));
      allelementVerifiers.remove(element);
    }
    else if("border-radius".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[65]=new CSSPropertyVerifier(Arrays.asList("/"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("64<1,4>", "64<1,4> 65 64<1,4>")));
      allelementVerifiers.remove(element);
    }
    else if("border-top-radius".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("64<1,2>")));
      allelementVerifiers.remove(element);
    }
    else if("border-bottom-radius".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("64<1,2>")));
      allelementVerifiers.remove(element);
    }
    else if("border-left-radius".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("64<1,2>")));
      allelementVerifiers.remove(element);
    }
    else if("border-right-radius".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("64<1,2>")));
      allelementVerifiers.remove(element);
    }
    else if("border-image-source".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("76")));
      allelementVerifiers.remove(element);
    }
    else if("border-image-slice".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[66]=new CSSPropertyVerifier(null,Arrays.asList("pe","in"),null,null,true);
      auxilaryVerifiers[67]=new CSSPropertyVerifier(Arrays.asList("fill"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("66<1,4> 67?")));
      allelementVerifiers.remove(element);
    }
    else if("border-image-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("77")));
      allelementVerifiers.remove(element);
    }
    else if("border-image-outset".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[69]=new CSSPropertyVerifier(null,Arrays.asList("le","in"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("69<1,4>")));
      allelementVerifiers.remove(element);
    }
    else if("border-image-repeat".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("78")));
      allelementVerifiers.remove(element);
    }
    else if("border-image".equalsIgnoreCase(element))
    { // FIXME: css3: not sure how to do the rest
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("76a77a78")));
      allelementVerifiers.remove(element);
    }
    else if("bottom".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);

    }
    else if("box-decoration-break".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("slice","clone"),ElementInfo.VISUALMEDIA,null));
      allelementVerifiers.remove(element);

    }
    else if("box-shadow".equalsIgnoreCase(element))
    { // way more permissive than it should be
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"), ElementInfo.VISUALMEDIA, null, Arrays.asList("75<1,65535>"), true, true));
      allelementVerifiers.remove(element);

    }
    else if("caption-side".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("top","bottom","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);

    }
    else if("clear".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","left","right","both","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("clip".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("sh")));
      allelementVerifiers.remove(element);
    }
                else if("break-after".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","always","avoid","left","right", "page", "column", "avoid-page", "avoid-column" ),ElementInfo.VISUALPAGEDMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("break-before".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","always","avoid","left","right", "page", "column", "avoid-page", "avoid-column" ),ElementInfo.VISUALPAGEDMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("break-inside".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","avoid","avoid-page", "avoid-column"),ElementInfo.VISUALPAGEDMEDIA));
      allelementVerifiers.remove(element);
    }
                else if("column-count".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto"),ElementInfo.VISUALMEDIA,Arrays.asList("in")));
      allelementVerifiers.remove(element);
                }
                else if("column-fill".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto", "balance"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
                }
                else if("column-gap".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal"),ElementInfo.VISUALMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
                }
                else if("column-rule-color".equalsIgnoreCase(element))
                {

      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);
                }
                else if("column-rule-style".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("13<1,4>")));
      allelementVerifiers.remove(element);
                }
                else if("column-rule-width".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("14<1,4>")));
      allelementVerifiers.remove(element);
                }
    else if("column-rule".equalsIgnoreCase(element))
                {
      // column-rule-width
      auxilaryVerifiers[54] = new CSSPropertyVerifier(null,null,null,Arrays.asList("14<1,4>"));
      // border-style
      auxilaryVerifiers[55] = new CSSPropertyVerifier(null,null,null,Arrays.asList("13<1,4>"));
      // color || transparent 13
                        elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("54a55a15")));
      allelementVerifiers.remove(element);
                }
                else if("column-span".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("1", "all"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
                }
                else if("column-width".equalsIgnoreCase(element))
                {
                        elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto"),ElementInfo.VISUALMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
                }
    else if("columns".equalsIgnoreCase(element))
    {
      // column-width
      auxilaryVerifiers[52]=new CSSPropertyVerifier(Arrays.asList("auto"),Arrays.asList("le"),null,null,true);
      // column-count
      auxilaryVerifiers[53]=new CSSPropertyVerifier(Arrays.asList("auto"),Arrays.asList("in"),null,null,true);

      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("52a53")));
      allelementVerifiers.remove(element);
    }
                else if ("color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);

    }
    else if ("color-interpolation".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","sRGB","linearRGB","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);

    }
    else if ("color-rendering".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","optimizeSpeed","optimizeQuality","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);

    }
    else if("content".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[16]=new ContentPropertyVerifier(Arrays.asList("open-quote","close-quote","no-open-quote", "no-close-quote"));
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","none","inherit"),ElementInfo.MEDIA,null,Arrays.asList("16<1,"+ ElementInfo.UPPERLIMIT+">")));
      allelementVerifiers.remove(element);
    }
    else if("counter-increment".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[17]=new CSSPropertyVerifier(null,Arrays.asList("id"),null,null,true);
      auxilaryVerifiers[18]=new CSSPropertyVerifier(null,Arrays.asList("in"),null,null,true);
      auxilaryVerifiers[19]=new CSSPropertyVerifier(null,null,Arrays.asList("17 18?"),null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.MEDIA,null,Arrays.asList("19<1,"+ElementInfo.UPPERLIMIT+">[1,2]")));
      allelementVerifiers.remove(element);
    }
    else if("counter-reset".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[20]=new CSSPropertyVerifier(null,Arrays.asList("id"),null,null,true);
      auxilaryVerifiers[21]=new CSSPropertyVerifier(null,Arrays.asList("in"),null,null,true);
      auxilaryVerifiers[22]=new CSSPropertyVerifier(null,null,Arrays.asList("20 21?"),null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.MEDIA,null,Arrays.asList("22<1,"+ElementInfo.UPPERLIMIT+">[1,2]")));
      allelementVerifiers.remove(element);
    }
    else if("cue-after".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.AURALMEDIA,Arrays.asList("ur")));
      allelementVerifiers.remove(element);

    }
    else if("cue-before".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.AURALMEDIA,Arrays.asList("ur")));
      allelementVerifiers.remove(element);
    }
    else if("cue".equalsIgnoreCase(element))
    {
      //cue-before
      auxilaryVerifiers[23]=new CSSPropertyVerifier(Arrays.asList("none","inherit"),Arrays.asList("ur"),null,null,true);
      //cue-after
      auxilaryVerifiers[24]=new CSSPropertyVerifier(Arrays.asList("none","inherit"),Arrays.asList("ur"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.MEDIA,null,Arrays.asList("23a24")));
      allelementVerifiers.remove(element);

    }
    else if("cursor".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[25]=new CSSPropertyVerifier(null,Arrays.asList("ur"),null,null,true);
      auxilaryVerifiers[26]=new CSSPropertyVerifier(Arrays.asList("auto","crosshair","default","pointer","move","e-resize","ne-resize","nw-resize","n-resize","se-resize","sw-resize","s-resize","w-resize","text","wait","help","progress"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALINTERACTIVEMEDIA,null,Arrays.asList("25<0,"+ElementInfo.UPPERLIMIT+"> 26"),false,true));

      allelementVerifiers.remove(element);
    }
    else if("direction".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("ltr","rtl","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("display".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inline","block","list-item","run-in","inline-block","table","inline-table","table-row-group","table-header-group","table-footer-group","table-row","table-column-group","table-column","table-cell","table-caption","none","inherit"),ElementInfo.MEDIA));
      allelementVerifiers.remove(element);
    }
    else if("elevation".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("below","level","above","higher","lower", "inherit"),ElementInfo.AURALMEDIA,Arrays.asList("an")));
      allelementVerifiers.remove(element);
    }
    else if("empty-cells".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("show","hide","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("float".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("left","right","none","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("font-family".equalsIgnoreCase(element))
    {

      elementVerifiers.put(element,new FontPropertyVerifier(false));
      allelementVerifiers.remove(element);
    }
    else if("font-size".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("xx-small","x-small","small","medium","large","x-large","xx-large","larger","smaller","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("font-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","italic","oblique","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("font-variant".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","small-caps","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("font-weight".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","bold","bolder","lighter","100","200","300","400","500","600","700","800","900","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("font".equalsIgnoreCase(element))
    {

      //font-style
      auxilaryVerifiers[27]=new CSSPropertyVerifier(Arrays.asList("normal","italic","oblique","inherit"),null,null,null,true);
      //font-variant
      auxilaryVerifiers[28]=new CSSPropertyVerifier(Arrays.asList("normal","small-caps","inherit"),null,null,null,true);
      //font-weight
      auxilaryVerifiers[29]=new CSSPropertyVerifier(Arrays.asList("normal","bold","bolder","lighter","100","200","300","400","500","600","700","800","900","inherit"),null,null,null,true);
      //30-32
      auxilaryVerifiers[30]=new CSSPropertyVerifier(null,null,Arrays.asList("27a28a29"),null,true);
      /*
      //font-size
      auxilaryVerifiers[31]=new CSSPropertyVerifier(Arrays.asList("xx-small","x-small","small","medium","large","x-large","xx-large","larger","smaller","inherit"),null,null,true);
      //line-height
      auxilaryVerifiers[32]=new CSSPropertyVerifier(Arrays.asList("normal","inherit"),Arrays.asList("le","pe","re","in"),null,true);

      auxilaryVerifiers[55]=new CSSPropertyVerifier(Arrays.asList("/"),null,null,true);
      auxilaryVerifiers[56]=new CSSPropertyVerifier(null,null,Arrays.asList("55 32"),true);
       */
      auxilaryVerifiers[31]=new FontPartPropertyVerifier();
      //font-family
      auxilaryVerifiers[59]=new FontPropertyVerifier(true);


      /*
       * old font family
      auxilaryVerifiers[53]=new CSSPropertyVerifier(ElementInfo.FONTS,null,null,true);
      auxilaryVerifiers[54]=new CSSPropertyVerifier(Arrays.asList("inherit"),null,Arrays.asList("53 53<0,"+ElementInfo.UPPERLIMIT+">"),true);
       */
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("caption","icon","menu","message-box","small-caption","status-bar","inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("30<0,1>[1,3] 31<0,1>[1,3] 59"),false,true));
      //elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("caption","icon","menu","message-box","small-caption","status-bar","inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("31<1,1>[1,3]")));
      allelementVerifiers.remove(element);
    }
    else if("hanging-punctuation".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[97]=new CSSPropertyVerifier(Arrays.asList("allow-end","force-end"),null,null,null,true);
      auxilaryVerifiers[98]=new CSSPropertyVerifier(Arrays.asList("first"),null,null,null,true);
      auxilaryVerifiers[99]=new CSSPropertyVerifier(Arrays.asList("last"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,null,Arrays.asList("97a98a99")));
      allelementVerifiers.remove(element);
    }
    else if("height".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("left".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("letter-spacing".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),null,ElementInfo.VISUALMEDIA,Arrays.asList("85<1,3>")));
      allelementVerifiers.remove(element);
    }
    else if("line-height".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe","re","in")));
      allelementVerifiers.remove(element);
    }
    else if("line-break".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","newspaper","normal","strict","keep-all"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("list-style-image".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("ur")));
      allelementVerifiers.remove(element);
    }
    else if("list-style-position".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inside","outside","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("list-style-type".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("disc","circle","square","decimal","decimal-leading-zero","lower-roman","upper-roman","lower-greek","lower-latin","upper-latin","armenian","georgian","lower-alpha","upper-alpha","none","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("list-style".equalsIgnoreCase(element))
    {
      //list-style-image
      auxilaryVerifiers[33]=new CSSPropertyVerifier(Arrays.asList("none","inherit"),Arrays.asList("ur"),null,null,true);
      //list-style-position
      auxilaryVerifiers[34]=new CSSPropertyVerifier(Arrays.asList("inside","outside","inherit"),null,null,null,true);
      //list-style-type
      auxilaryVerifiers[35]=new CSSPropertyVerifier(Arrays.asList("disc","circle","square","decimal","decimal-leading-zero","lower-roman","upper-roman","lower-greek","lower-latin","upper-latin","armenian","georgian","lower-alpha","upper-alpha","none","inherit"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("33a34a35")));
      allelementVerifiers.remove(element);
    }
    else if("margin-right".equalsIgnoreCase(element))
    {
      //margin-width=Length|Percentage|Auto
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("margin-left".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("margin-top".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("margin-bottom".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("margin".equalsIgnoreCase(element))
    {
      //margin-width
      auxilaryVerifiers[36]=new CSSPropertyVerifier(Arrays.asList("auto","inherit"),Arrays.asList("le","pe"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("36<1,4>")));
      allelementVerifiers.remove(element);
    }
    else if("max-height".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("max-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("min-height".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("min-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("opacity".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALPAGEDMEDIA,Arrays.asList("re")));
      allelementVerifiers.remove(element);
    }
    else if("orphans".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALPAGEDMEDIA,Arrays.asList("in")));
      allelementVerifiers.remove(element);
    }
    else if("outline-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("invert", "inherit"),ElementInfo.VISUALINTERACTIVEMEDIA,Arrays.asList("co")));
      allelementVerifiers.remove(element);
    }
    else if("outline-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),ElementInfo.VISUALINTERACTIVEMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("outline-width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("thin","medium","thick","inherit"),ElementInfo.VISUALINTERACTIVEMEDIA,Arrays.asList("le")));
      allelementVerifiers.remove(element);
    }
    else if("outline".equalsIgnoreCase(element))
    {
      //outline-color
      auxilaryVerifiers[37]=new CSSPropertyVerifier(Arrays.asList("invert", "inherit"),Arrays.asList("co"),null,null,true);
      //outline-style
      auxilaryVerifiers[38]=new CSSPropertyVerifier(Arrays.asList("none","hidden","dotted","dashed","solid","double","groove","ridge","inset","outset", "inherit"),null,null,null,true);
      //outline-width
      auxilaryVerifiers[39]=new CSSPropertyVerifier(Arrays.asList("thin","medium","thick","inherit"),Arrays.asList("le"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALINTERACTIVEMEDIA,Arrays.asList("le"),Arrays.asList("37a38a39")));
      allelementVerifiers.remove(element);
    }
    else if("overflow".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("visible","hidden","scroll","auto","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("padding-top".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);

    }
    else if("padding-right".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("padding-bottom".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("padding-left".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("padding".equalsIgnoreCase(element))
    {
      //padding-width
      auxilaryVerifiers[40]=new CSSPropertyVerifier(Arrays.asList("inherit"),Arrays.asList("le","pe"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("40<1,4>")));
      allelementVerifiers.remove(element);
    }
    else if("page-break-after".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","always","avoid","left","right","inherit"),ElementInfo.VISUALPAGEDMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("page-break-before".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","always","avoid","left","right","inherit"),ElementInfo.VISUALPAGEDMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("page-break-inside".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","avoid","inherit"),ElementInfo.VISUALPAGEDMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("pause-after".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.AURALMEDIA,Arrays.asList("ti","pe")));
      allelementVerifiers.remove(element);
    }
    else if("pause-before".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.AURALMEDIA,Arrays.asList("ti","pe")));
      allelementVerifiers.remove(element);
    }
    else if("pause".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[41]=new CSSPropertyVerifier(Arrays.asList("inherit"),Arrays.asList("ti","pe"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),null,null,Arrays.asList("41<1,2>")));
      allelementVerifiers.remove(element);

    }
    else if("pitch-range".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.AURALMEDIA,Arrays.asList("in","re")));
      allelementVerifiers.remove(element);
    }
    else if("pitch".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("x-low","low","medium","high","x-high","inherit"),ElementInfo.AURALMEDIA,Arrays.asList("fr")));
      allelementVerifiers.remove(element);

    }
    else if("play-during".equalsIgnoreCase(element))
    {

      auxilaryVerifiers[42]=new CSSPropertyVerifier(null,Arrays.asList("ur"),null,null,true);
      auxilaryVerifiers[43]=new CSSPropertyVerifier(Arrays.asList("mix"),null,null,null,true);
      auxilaryVerifiers[44]=new CSSPropertyVerifier(Arrays.asList("repeat"),null,null,null,true);
      auxilaryVerifiers[45]=new CSSPropertyVerifier(null,null,Arrays.asList("43a44"),null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","none","inherit"),ElementInfo.AURALMEDIA,null,Arrays.asList("42 45<0,1>[1,2]")));
      allelementVerifiers.remove(element);


    }
    else if("punctuation-trim".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[86]=new CSSPropertyVerifier(Arrays.asList("start"),null,null,null,true);
      auxilaryVerifiers[87]=new CSSPropertyVerifier(Arrays.asList("end","allow-end"),null,null,null,true);
      auxilaryVerifiers[88]=new CSSPropertyVerifier(Arrays.asList("adjacent"),null,null,null,true);
      auxilaryVerifiers[89]=new CSSPropertyVerifier(null,null,Arrays.asList("86a87a88"),null,true);

      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.AURALMEDIA,null,Arrays.asList("89")));
      allelementVerifiers.remove(element);
    }
    else if("position".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("static","relative","absolute","fixed","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("quotes".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[46]=new CSSPropertyVerifier(null,Arrays.asList("st"),null,null,true);
      auxilaryVerifiers[47]=new CSSPropertyVerifier(null,null,Arrays.asList("46 46"),null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none","inherit"),null,ElementInfo.VISUALMEDIA,Arrays.asList("47<1,"+ ElementInfo.UPPERLIMIT+">[2,2]")));
      allelementVerifiers.remove(element);
    }
    else if("richness".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.AURALMEDIA,Arrays.asList("re","in")));
      allelementVerifiers.remove(element);
    }
    else if("right".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("speak-header".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("once","always","inherit"),ElementInfo.AURALMEDIA));
      allelementVerifiers.remove(element);

    }
    else if("speak-numeral".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("digits","continuous","inherit"),ElementInfo.AURALMEDIA));
      allelementVerifiers.remove(element);

    }
    else if("speak-punctuation".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("code", "none","inherit"),ElementInfo.AURALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("speak".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","none","spell-out","inherit"),ElementInfo.AURALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("speech-rate".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("x-slow","slow","medium","fast","x-fast","faster","slower","inherit"),ElementInfo.AURALMEDIA,Arrays.asList("re","in")));
      allelementVerifiers.remove(element);
    }
    else if("stress".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.AURALMEDIA,Arrays.asList("re","in")));
      allelementVerifiers.remove(element);
    }
    else if("table-layout".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("auto","fixed","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("text-align".equalsIgnoreCase(element))
    {  // FIXME: We don't support "one character" as the spec says http://www.w3.org/TR/css3-text/#text-align0
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("start","end","left","right","center","justify","match-parent","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("text-align-last".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("start","end","left","right","center","justify"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("text-autospace".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[90]=new CSSPropertyVerifier(Arrays.asList("ideograph-numeric"),null,null,null,true);
      auxilaryVerifiers[91]=new CSSPropertyVerifier(Arrays.asList("ideograph-alpha"),null,null,null,true);
      auxilaryVerifiers[92]=new CSSPropertyVerifier(Arrays.asList("ideograph-space"),null,null,null,true);
      auxilaryVerifiers[93]=new CSSPropertyVerifier(Arrays.asList("ideograph-parenthesis"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,null,Arrays.asList("90a91a92a93")));
      allelementVerifiers.remove(element);

    }
    else if("text-decoration".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("115a103a104a116")));
      allelementVerifiers.remove(element);

    }
    else if("text-decoration-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("103")));
      allelementVerifiers.remove(element);

    }
    else if("text-decoration-line".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,null,Arrays.asList("100a101a102")));
      allelementVerifiers.remove(element);

    }
    else if("text-decoration-skip".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[48]=new CSSPropertyVerifier(Arrays.asList("images"),null,null,null,true);
      auxilaryVerifiers[49]=new CSSPropertyVerifier(Arrays.asList("spaces"),null,null,null,true);
      auxilaryVerifiers[50]=new CSSPropertyVerifier(Arrays.asList("ink"),null,null,null,true);
      auxilaryVerifiers[51]=new CSSPropertyVerifier(Arrays.asList("all"),null,null,null,true);

      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,null,Arrays.asList("48a49a50a51")));
      allelementVerifiers.remove(element);
    }
    else if("text-decoration-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("104")));
      allelementVerifiers.remove(element);

    }
    else if("text-emphasis".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("103a107")));
      allelementVerifiers.remove(element);

    }
    else if("text-emphasis-color".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("103")));
      allelementVerifiers.remove(element);

    }
    else if("text-emphasis-position".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("over","under"),ElementInfo.VISUALMEDIA,null,null));
      allelementVerifiers.remove(element);

    }
    else if("text-emphasis-style".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("107")));
      allelementVerifiers.remove(element);

    }
    else if("text-indent".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[94]=new CSSPropertyVerifier(Arrays.asList("hanging", "each-line"),null,null,null,true);
      auxilaryVerifiers[95]=new CSSPropertyVerifier(null,null,Arrays.asList("94<0,2>"),null,true);
      auxilaryVerifiers[96]=new CSSPropertyVerifier(null,Arrays.asList("le","pe"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("96 95")));
      allelementVerifiers.remove(element);
    }
    else if("text-justify".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[83]=new CSSPropertyVerifier(Arrays.asList("inter-word","inter-ideograph","inter-cluster","distribute","kashida"),null,null,null,true);
      auxilaryVerifiers[84]=new CSSPropertyVerifier(Arrays.asList("trim"),null,null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto" ,"inherit"),ElementInfo.VISUALMEDIA,null,Arrays.asList("84a83")));
      allelementVerifiers.remove(element);
    }
    else if("text-outline".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[108]=new CSSPropertyVerifier(null,null,Arrays.asList("73 72 72<0,1>"),null,true);
      auxilaryVerifiers[109]=new CSSPropertyVerifier(null,null,Arrays.asList("72 72<0,1> 73"),null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,null,Arrays.asList("108a109")));
      allelementVerifiers.remove(element);
    }
    else if("text-overflow".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("clip","ellipsis"),ElementInfo.VISUALMEDIA,Arrays.asList("st")));
      allelementVerifiers.remove(element);
    }
    else if("text-shadow".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("none"),ElementInfo.VISUALMEDIA,null,Arrays.asList("79<0,65535>"),true,true));
      allelementVerifiers.remove(element);
    }
    else if("text-transform".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("capitalize","uppercase","lowercase","none","inherit","fullwidth","large-kana"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("text-underline-position".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("auto","under","alphabetic","over"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("text-wrap".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("normal","unrestricted","none","suppress"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("top".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("transform".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[110]=new CSSPropertyVerifier(null,Arrays.asList("tr"),null,null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("110<0,65536>"),true, true));
      allelementVerifiers.remove(element);
    }
    else if("transform-origin".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[111]=new CSSPropertyVerifier(null,null,Arrays.asList("2 3<0,1>"),null,true);
      auxilaryVerifiers[112]=new CSSPropertyVerifier(Arrays.asList("left","center","right"),null,null,null,true);
      auxilaryVerifiers[113]=new CSSPropertyVerifier(Arrays.asList("top","center","bottom"),null,null,null,true);
      auxilaryVerifiers[114]=new CSSPropertyVerifier(null,null,Arrays.asList("112a113"),null,true);

      elementVerifiers.put(element,new CSSPropertyVerifier(null,ElementInfo.VISUALMEDIA,null,Arrays.asList("111","114"),true, true));
      allelementVerifiers.remove(element);
    }
    else if("unicode-bidi".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier( Arrays.asList("normal", "embed", "bidi-override","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("vertical-align".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("baseline","sub","super","top","text-top","middle","bottom","text-bottom","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("pe","le")));
      allelementVerifiers.remove(element);
    }
    else if("visibility".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("visible","hidden","collapse","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("voice-family".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new VoiceFamilyPropertyVerifier(false));
      allelementVerifiers.remove(element);
    }
    else if("volume".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("silent","x-soft","soft","medium","loud","x-loud","inherit"),ElementInfo.AURALMEDIA,Arrays.asList("re","le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("white-space".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","pre","nowrap","pre-wrap","pre-line","inherit"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("white-space-collapsing".equalsIgnoreCase(element))
    {
      auxilaryVerifiers[80]=new CSSPropertyVerifier(Arrays.asList("preserve","preserve-break"),null,null,null,true);
      auxilaryVerifiers[81]=new CSSPropertyVerifier(Arrays.asList("trim-inner"),null,null,null,true);
      auxilaryVerifiers[82]=new CSSPropertyVerifier(null,null,Arrays.asList("80a81"),null,true);
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("collapse" ,"discard"),null,ElementInfo.VISUALMEDIA,Arrays.asList("82")));
      allelementVerifiers.remove(element);
    }
    else if("widows".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("in")));
      allelementVerifiers.remove(element);
    }
    else if("width".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("le","pe")));
      allelementVerifiers.remove(element);
    }
    else if("word-break".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal","break-all","hyphenate"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("word-spacing".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("inherit"),null,ElementInfo.VISUALMEDIA,Arrays.asList("85<1,3>")));
      allelementVerifiers.remove(element);
    }
    else if("word-wrap".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("normal", "break-word"),ElementInfo.VISUALMEDIA));
      allelementVerifiers.remove(element);
    }
    else if("z-index".equalsIgnoreCase(element))
    {
      elementVerifiers.put(element,new CSSPropertyVerifier(Arrays.asList("auto","inherit"),ElementInfo.VISUALMEDIA,Arrays.asList("in")));
      allelementVerifiers.remove(element);
    }
  }


  /*
   * This function returns the Verifier for a property. If it is not already loaded in the elementVerifier, then it is loaded and then returned to the caller.
   * FIXME: Lazy init probably doesn't make sense, but while we are initting lazily, we need to hold a lock here.
   */
  private synchronized static CSSPropertyVerifier getVerifier(String element)
  {
    element=element.toLowerCase();
    if(elementVerifiers.get(element)!=null)
      return elementVerifiers.get(element);
    else if(allelementVerifiers.contains(element))
    {
      addVerifier(element);
      return elementVerifiers.get(element);
    }
    else
      return null;
  }
  /*
   * This function accepts media, list of HTML elements, CSS property and value and determines whether it is valid or not.
   * @media print
   * {
   * h1, h2 , h4, h4 {font-size: 10pt}
   * }
   * media: print
   * elements: [h1, h2, h3, h4]
   * token: font-size
   * value: 10pt
   *
   */
  private boolean verifyToken(String[] media,String[] elements,CSSPropertyVerifier obj,ParsedWord[] words)
  {
    if(words == null) return false;
    if(logDEBUG) Logger.debug(this, "verifyToken for "+CSSPropertyVerifier.toString(words));
    if(obj==null)
    {
      return false;
    }
    int important = checkImportant(words);
    if(important > 0) {
      if(words.length == important) return true; // Eh? !important on its own!
      words = Arrays.copyOf(words, words.length-important);
    }
    return obj.checkValidity(media, elements, words, cb);

  }

  private int checkImportant(ParsedWord[] words) {
    if(words.length == 0) return 0;
    if(words.length >= 1 && words[words.length-1] instanceof SimpleParsedWord) {
      if(((SimpleParsedWord)words[words.length-1]).original.equalsIgnoreCase("!important")) return 1;
    }
    if(words.length >= 2 && words[words.length-1] instanceof ParsedIdentifier && words[words.length-2] instanceof SimpleParsedWord) {
      if(((SimpleParsedWord)words[words.length-2]).original.equals("!") &&
        ((ParsedIdentifier)words[words.length-1]).original.equalsIgnoreCase("important"))
        return 2;
    }
    return 0;
  }

  /*
   * This function accepts an HTML element(along with class name, ID, pseudo class and attribute selector) and determines whether it is valid or not.
   * Returns null on failure (invalid selector), empty string on banned (but otherwise valid) selector.
   */
  public String HTMLelementVerifier(String elementString)
  {
    if(logDEBUG) Logger.debug(this, "varifying element/selector: \""+elementString+"\"");
    String HTMLelement="",pseudoClass="",className="",id="";
    boolean isValid=true;
    StringBuilder fBuffer=new StringBuilder();
    ArrayList<String> attSelections = null;
    while(elementString.indexOf('[')!=-1 && elementString.indexOf(']')!=-1 && (elementString.indexOf('[')<elementString.indexOf(']')))
    {
      String attSelection=elementString.substring(elementString.indexOf('[')+1,elementString.indexOf(']')).trim();
      StringBuilder buf=new StringBuilder(elementString);
      buf.delete(elementString.indexOf('['), elementString.indexOf(']')+1);
      elementString=buf.toString();
      if(logDEBUG) Logger.debug(this, "attSelection="+attSelection+"  elementString="+elementString);
      if(attSelections == null) attSelections = new ArrayList<String>();
      attSelections.add(attSelection);
    }
    if(elementString.indexOf(':')!=-1)
    {
      int index=elementString.indexOf(':');
      if(index!=elementString.length()-1)
      {
        pseudoClass=elementString.substring(index+1,elementString.length()).trim();
        HTMLelement=elementString.substring(0,index).trim();
        if(logDEBUG) Logger.debug(this, "pseudoclass="+pseudoClass+" HTMLelement="+HTMLelement);
      }
      else
      {
        HTMLelement=elementString.trim();
      }
    }
    else
      HTMLelement=elementString.trim();

    if(HTMLelement.indexOf('.')!=-1)
    {
      int index=HTMLelement.indexOf('.');
      if(index!=HTMLelement.length()-1)
      {
        className=HTMLelement.substring(index+1,HTMLelement.length()).trim();
        HTMLelement=HTMLelement.substring(0,index).trim();
        if(logDEBUG) Logger.debug(this, "class="+className+" HTMLelement="+HTMLelement);
      }

    }
    else if(HTMLelement.indexOf('#')!=-1)
    {
      int index=HTMLelement.indexOf('#');
      if(index!=HTMLelement.length()-1)
      {
        id=HTMLelement.substring(index+1,HTMLelement.length()).trim();
        HTMLelement=HTMLelement.substring(0,index).trim();
        if(logDEBUG) Logger.debug(this, "id="+id+" element="+HTMLelement);
      }

    }

    if("*".equals(HTMLelement) || (ElementInfo.isValidHTMLTag(HTMLelement.toLowerCase())) ||
        ("".equals(HTMLelement.trim()) &&
            ((!className.equals("")) || (!id.equals("")) || attSelections!=null || !pseudoClass.equals(""))))
    {
      if(!className.equals(""))
      {
        // Note that the definition of isValidName() allows chained classes because it allows . in class names.
        if(!ElementInfo.isValidName(className))
          isValid=false;
      }
      else if(!id.equals(""))
      {
        if(!ElementInfo.isValidName(id))
          isValid=false;
      }

      if(isValid && !pseudoClass.equals(""))
      {
        if(!ElementInfo.isValidPseudoClass(pseudoClass)) {
          isValid=false;
        } else if(ElementInfo.isBannedPseudoClass(pseudoClass)) {
          return "";
        }
      }

      if(isValid && attSelections!=null)
      {
        String[] attSelectionParts;

        for(String attSelection : attSelections) {
          if(attSelection.indexOf("|=")!=-1)
          {
            attSelectionParts=new String[2];
            attSelectionParts[0]=attSelection.substring(0,attSelection.indexOf("|="));
            attSelectionParts[1]=attSelection.substring(attSelection.indexOf("|=")+2,attSelection.length());
          }
          else if(attSelection.indexOf("~=")!=-1) {
            attSelectionParts=new String[2];
            attSelectionParts[0]=attSelection.substring(0,attSelection.indexOf("~="));
            attSelectionParts[1]=attSelection.substring(attSelection.indexOf("~=")+2,attSelection.length());
          } else if(attSelection.indexOf('=') != -1){
            attSelectionParts=new String[2];
            attSelectionParts[0]=attSelection.substring(0,attSelection.indexOf('='));
            attSelectionParts[1]=attSelection.substring(attSelection.indexOf('=')+1,attSelection.length());
          } else {
            attSelectionParts=new String[] { attSelection };
          }

          //Verifying whether each character is alphanumeric or _
          if(logDEBUG) Logger.debug(this, "HTMLelementVerifier length of attSelectionParts="+attSelectionParts.length);

          if(attSelectionParts[0].length()==0)
            isValid=false;
          else
          {
            char c=attSelectionParts[0].charAt(0);
            if(!((c>='a' && c<='z') || (c>='A' && c<='Z')))
              isValid=false;
            for(int i=1;i<attSelectionParts[0].length();i++)
            {
              if(!((c>='a' && c<='z') || (c>='A' && c<='Z') || c=='_' || c=='-'))
                isValid=false;
            }
          }

          if(attSelectionParts.length > 1) {
            // What about the right hand side?
            // The grammar says it's an IDENT.
            if(logDEBUG) Logger.debug(this, "RHS is \""+attSelectionParts[1]+"\"");
            if(!(ElementInfo.isValidIdentifier(attSelectionParts[1]) || ElementInfo.isValidStringWithQuotes(attSelectionParts[1]))) isValid = false;
          }
        }
      }


      if(isValid)
      {
        fBuffer.append(HTMLelement);
        if(!className.equals("")) {
          fBuffer.append('.');
          fBuffer.append(className);
        } else if(!id.equals("")) {
          fBuffer.append('#');
          fBuffer.append(id);
        }
        if(!pseudoClass.equals("")) {
          fBuffer.append(':');
          fBuffer.append(pseudoClass);
        }
        if(attSelections!=null) {
          for(String attSelection:attSelections) {
            fBuffer.append('[');
            fBuffer.append(attSelection);
            fBuffer.append(']');
          }
        }
        return fBuffer.toString();
      }
    }

    return null;
  }
  /*
   * This function works with different operators, +, >, " " and verifies each HTML element with HTMLelementVerifier(String elementString)
   * e.g. div > p:first-child
   * This would call HTMLelementVerifier with div and p:first-child
   * Returns null on failure (selector invalid), empty string on banned but otherwise valid selector.
   */
  public String recursiveSelectorVerifier(String selectorString)
  {
    if(logDEBUG) Logger.debug(this, "selector: \""+selectorString+"\"");
    selectorString=selectorString.trim();

    // Parse but don't tokenise.

    int index = -1;
    char selector = 0;

    char c;
    char quoting = 0;
    boolean escaping = false;
    int bracketing = 0;
    boolean eatLF = false;
    int escapedDigits = 0;
    for(int i=0;i<selectorString.length();i++) {
      c = selectorString.charAt(i);
      if(c == '+' && quoting == 0 && !escaping && bracketing == 0) {
        if(index == -1 || index == i-1 && selector == ' ') {
          index = i;
          selector = c;
        }
      } else if(c == '>' && quoting == 0 && !escaping) {
        if(index == -1 || index == i-1 && selector == ' ') {
          index = i;
          selector = c;
        }
      } else if(c == ' ' && quoting == 0 && !escaping) {
        if(index == -1 || index == i-1 && selector == ' ') {
          index = i;
          selector = c;
        }
      } else if(c == '(' && quoting == 0 && !escaping) {
        bracketing += 1;
      } else if(c == ')' && quoting == 0 && !escaping) {
        bracketing -= 1;
      } else if(c == '\'' && quoting == 0 && !escaping) {
        quoting = c;
      } else if(c == '\"' && quoting == 0 && !escaping) {
        quoting = c;
      } else if(c == quoting && !escaping) {
        quoting = 0;
      } else if(c == '\n' && eatLF) {
        // Ok
        escaping = false;
        eatLF = false;
      } else if((c == '\r' || c == '\n' || c == '\f') && !(quoting != 0 && escaping)) {
        // No newlines unless in a string *and* quoted!
        if(logDEBUG) Logger.debug(this, "no newlines unless in a string *and* quoted at index "+i);
        return null;
      } else if(c == '\r' && escaping && escapedDigits == 0) {
        escaping = false;
        eatLF = true;
      } else if((c == '\n' || c == '\f') && escaping) {
        if(escapedDigits == 0)
          escaping = false;
        else {
          if(logDEBUG) Logger.debug(this, "invalid newline escaping at char "+i);
          return null; // Invalid
        }
      } else if(escaping && ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
        escapedDigits++;
        if(escapedDigits == 6)
          escaping = false;
      } else if(escaping && escapedDigits > 0 && (" \t\r\n\f".indexOf(c) != -1)) {
        escaping = false;
        if(c == '\r') eatLF = true;
      } else if(c == '\\' && !escaping) {
        escaping = true;
      } else if(c == '\\' && escaping && escapedDigits > 0) {
        if(logDEBUG) Logger.debug(this, "backslash but already escaping with digits at char "+i);
        return null; // Invalid
      } else if(c == '\\' && escaping) {
        escaping = false;
      } else if(escaping) {
        // Any other character can be escaped.
        escaping = false;
      }
      eatLF = false;
    }

    if(logDEBUG) Logger.debug(this, "index="+index+" quoting="+quoting+" selector="+selector+" for \""+selectorString+"\"");

    if(quoting != 0) return null; // Mismatched quotes
    if(bracketing != 0) return null; // Mismatched brackets

    if(index == -1)
      return HTMLelementVerifier(selectorString);

    String[] parts=new String[2];

    parts[0]=selectorString.substring(0,index).trim();
    parts[1]=selectorString.substring(index+1,selectorString.length()).trim();
    if(logDEBUG) Logger.debug(this, "recursiveSelectorVerifier parts[0]=" + parts[0]+" parts[1]="+parts[1]);
    parts[0]=HTMLelementVerifier(parts[0]);
    parts[1]=recursiveSelectorVerifier(parts[1]);
    if(parts[0]!=null && parts[1]!=null)
      return parts[0]+selector+parts[1];
    else
      return null;

  }



  // main function
  public void parse() throws IOException {

    final int STATE1=1; //State corresponding to @page,@media etc
    final int STATE2=2; //State corresponding to HTML element like body
    final int STATE3=3; //State corresponding to CSS properties

    /* e.g.
     * STATE1
     * @media screen {
     * STATE2  STATE3
     * h2     {text-align:left;}
     * }
     */
    final int STATECOMMENT=4;
    final int STATE1INQUOTE=5;
    final int STATE2INQUOTE=6;
    final int STATE3INQUOTE=7;
    char currentQuote='"';
    int stateBeforeComment=0;
    int currentState=1;
    boolean isState1Present=false;
    String elements[]=null;
    StringBuilder filteredTokens=new StringBuilder();
    StringBuilder buffer=new StringBuilder();
    int openBraces=0;
    String defaultMedia="screen";
    String[] currentMedia=new String[] {defaultMedia};
    String propertyName="",propertyValue="";
    boolean ignoreElementsS1=false,ignoreElementsS2=false,ignoreElementsS3=false, closeIgnoredS2=false;
    int x;
    char c=0,prevc=0;
    boolean s2Comma=false;
    boolean canImport=true; //import statement can occur only in the beginning

    String whitespaceAfterColon = "";
    String whitespaceBeforeProperty = "";

    boolean charsetPossible = true;
    boolean bomPossible = true;
    int openBracesStartingS3 = 0;
    boolean forPage = false;

    if(isInline) {
      currentState = STATE3;
    }

    while(true)
    {
      try
      {
        x=r.read();
      }
      catch(IOException e)
      {
        throw e;
      }

      if(x==-1)
      {
        if(currentState == STATE3 && c != ';' && !propertyName.isEmpty() && propertyValue.isEmpty()) {
          // Finish off the current property.
          // Common for e.g. HTML: style='background-image:url(blah)'
          x = ';';
        } else {
          break;
        }
      }
      if(x == (char) 0xFEFF) {
        if(bomPossible) {
          // BOM
          if(logDEBUG) Logger.debug(this, "Ignoring BOM");
          w.write(x);
        }
        continue;
      }
      bomPossible = false;
      prevc=c;
      c=(char) x;
      if(logDEBUG) Logger.debug(this, "Read: "+c+ " 0x"+Integer.toHexString(c));
      if(prevc=='/' && c=='*' && currentState!=STATE1INQUOTE && currentState!=STATE2INQUOTE && currentState!=STATE3INQUOTE&&currentState!=STATECOMMENT)
      {
        stateBeforeComment=currentState;
        currentState=STATECOMMENT;
        if(buffer.charAt(buffer.length()-1)=='/')
        {
          buffer.deleteCharAt(buffer.length()-1);
        }
        if(logDEBUG) Logger.debug(this, "Comment detected: buffer="+buffer);
        prevc = 0;
      }
      if(c == 0)
        continue; // Strip nulls
      switch(currentState)
      {
      case STATE1:
        switch(c){
        case '\n':
        case ' ':
        case '\t':
          buffer.append(c);
          if(logDEBUG) Logger.debug(this, "STATE1 CASE whitespace: "+c);
          break;

        case '@':
          if(prevc != '\\') {
            isState1Present=true;
            if(logDEBUG) Logger.debug(this, "STATE1 CASE @: "+c);
          }
          buffer.append(c);
          break;

        case '{':
          charsetPossible=false;
          if(stopAtDetectedCharset)
            return;
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          openBraces++;
          isState1Present=false;

          int i = 0;
          for(i=0;i<buffer.length();i++) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          String braceSpace = buffer.substring(0, i);
          buffer.delete(0, i);
          if(buffer.length() > 4 && buffer.substring(0, 4).equals("<!--")) {
            braceSpace +=buffer.substring(0, 4);
            if(" \t\r\n".indexOf(buffer.charAt(4))==-1) {
              Logger.error(this, "<!-- not followed by whitespace!");
              return;
            }
            buffer.delete(0, 4);
            for(i=0;i<buffer.length();i++) {
              char c1 = buffer.charAt(i);
              if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
                continue;
              break;
            }
            braceSpace += buffer.substring(0, i);
            buffer.delete(0, i);
          }
          for(i=buffer.length()-1;i>=0;i--) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          i++;
          String postSpace = buffer.substring(i);
          buffer.setLength(i);
          String orig = buffer.toString().trim();
          ParsedWord[] parts=split(orig, false);
          if(logDEBUG) Logger.debug(this, "Split: "+CSSPropertyVerifier.toString(parts));
          buffer.setLength(0);
          boolean valid = false;
          if(parts != null) {
          if(parts.length<1)
          {
            ignoreElementsS1=true;
            if(logDEBUG) Logger.debug(this, "STATE1 CASE {: Does not have one part. ignoring "+buffer.toString());
            valid = false;
          }
          else if(parts[0] instanceof SimpleParsedWord && "@media".equals(((SimpleParsedWord)parts[0]).original.toLowerCase()))
          {
            if(parts.length<2)
            {
              ignoreElementsS1=true;
              if(logDEBUG) Logger.debug(this, "STATE1 CASE {: Does not have two parts. ignoring "+buffer.toString());
              valid = false;
            } else {
            ArrayList<String> medias = commaListFromIdentifiers(parts, 1);
            if(medias != null && medias.size() > 0) {
              for(i=0;i<medias.size();i++) {
                if(!FilterUtils.isMedia(medias.get(i))) {
                  // Unrecognised media, don't pass it.
                  medias.remove(i);
                  i--; // Don't skip next
                }
              }
            }
            if(medias != null && medias.size() > 0) {
              filteredTokens.append(braceSpace);
              filteredTokens.append("@media ");
              boolean first = true;
              for(String media : medias) {
                if(!first) filteredTokens.append(", ");
                first = false;
                filteredTokens.append(media);
              }
              filteredTokens.append(postSpace);
              filteredTokens.append("{");
              valid = true;
              currentMedia = medias.toArray(new String[medias.size()]);
            }
            }
          } else if(parts[0] instanceof SimpleParsedWord && "@page".equals(((SimpleParsedWord)parts[0]).original.toLowerCase()))
            {
            if(parts.length == 0) {
              valid = true;
            } else {
              valid = true;
              for(int j=1;j<parts.length;j++) {
                if(!(parts[j] instanceof SimpleParsedWord)) {
                  valid = false;
                  break;
                } else {
                  String s = ((SimpleParsedWord)parts[j]).original;
                  if(!(s.equalsIgnoreCase(":left") || s.equalsIgnoreCase(":right") || s.equals(":first"))) {
                    valid = false;
                    break;
                  }
                }
              }
            }
            if(valid) {
              forPage = true;
              filteredTokens.append(braceSpace);
              filteredTokens.append(orig);
              filteredTokens.append(postSpace);
              filteredTokens.append("{");
            }
          }
          } // else valid = false
          if(!valid)
          {
            ignoreElementsS1=true;
            // No valid media types.
            if(logDEBUG) Logger.debug(this, "STATE1 CASE {: Failed verification test. ignoring "+buffer.toString());
          } else {
            w.write(filteredTokens.toString());
            filteredTokens.setLength(0);
          }
          buffer.setLength(0);
          s2Comma=false;
          if(forPage) {
            currentState=STATE3;
            openBracesStartingS3 = openBraces;
          } else {
            currentState=STATE2;
          }
          buffer.setLength(0);
          break;
        case ';':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          if(logDEBUG) Logger.debug(this, "buffer in state 1 ; : \""+buffer.toString()+"\"");
          //should be @import

          for(i=0;i<buffer.length();i++) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          w.write(buffer.substring(0, i));
          buffer.delete(0, i);

          if(buffer.length() > 4 && buffer.substring(0, 4).equals("<!--")) {
            w.write(buffer.substring(0, 4));
            if(" \t\r\n".indexOf(buffer.charAt(4))==-1) {
              Logger.error(this, "<!-- not followed by whitespace!");
              return;
            }
            buffer.delete(0, 4);
            for(i=0;i<buffer.length();i++) {
              char c1 = buffer.charAt(i);
              if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
                continue;
              break;
            }
            w.write(buffer.substring(0, i));
            buffer.delete(0, i);
          }

          // If ignoreElementsS1, then just delete everything up to the semicolon. After that, fresh start.
          if(canImport && !ignoreElementsS1 && buffer.toString().contains("@import"))
          {
            if(logDEBUG) Logger.debug(this, "STATE1 CASE ;statement="+buffer.toString());

            String strbuffer=buffer.toString().trim();
            int importIndex=strbuffer.toLowerCase().indexOf("@import");
            if("".equals(strbuffer.substring(0,importIndex).trim()))
            {
              String str1=strbuffer.substring(importIndex+7,strbuffer.length());
              ParsedWord[] strparts=split(str1, false);
              if(strparts != null && strparts.length > 0 && (strparts[0] instanceof ParsedURL || strparts[0] instanceof ParsedString)) {
                String uri;
                if(strparts[0] instanceof ParsedString) {
                  uri = ((ParsedString)strparts[0]).getDecoded();
                } else {
                  uri = ((ParsedURL)strparts[0]).getDecoded();
                }
                ArrayList<String> medias = commaListFromIdentifiers(strparts, 1);

                if(medias != null) { // None gives [0], broke gives null
                  StringBuilder output = new StringBuilder();
                  output.append("@import url(\"");
                  try {
                    // Add ?maybecharset= even though there might be a ?type= with a charset, we will ignore maybecharset if there is.
                    // We behave similarly in <link rel=stylesheet...> if there is a ?type= in the URL.
                    String s = cb.processURI(uri, "text/css");
                    if(passedCharset != null) {
                      if(s.indexOf('?') == -1)
                        s += "?maybecharset="+passedCharset;
                      else
                        s += "&maybecharset="+passedCharset;
                    }
                    output.append(s);
                    output.append("\")");
                    boolean first = true;
                    for(String media : medias) {
                      if(FilterUtils.isMedia(media)) {
                        if(!first) output.append(", ");
                        else output.append(' ');
                        first = false;
                        output.append(media);
                      }
                    }
                    output.append(";");
                    w.write(output.toString());
                  } catch (CommentException e) {
                    // Don't write anything
                  }
                }
              }
            }
          } else if(charsetPossible && buffer.toString().startsWith("@charset ")) {
            // charsetPossible is incompatible with ignoreElementsS1
            String s = buffer.delete(0, "@charset ".length()).toString();
            s = removeOuterQuotes(s);
            detectedCharset = s;
            if(logDEBUG) Logger.debug(this, "Detected charset: \""+detectedCharset+"\"");
            if(!Charset.isSupported(detectedCharset)) {
              Logger.normal(this, "Charset not supported: "+detectedCharset);
              throw new UnsupportedCharsetInFilterException("Charset not supported: "+detectedCharset);
            }
            if(stopAtDetectedCharset) return;
            if(passedCharset != null && !detectedCharset.equalsIgnoreCase(passedCharset)) {
              Logger.normal(this, "Detected charset \""+detectedCharset+"\" differs from passed in charset \""+passedCharset+"\"");
              throw new IOException("Detected charset differs from passed in charset");
            }
            w.write("@charset \""+detectedCharset+"\";");
          }
          isState1Present=false;
          ignoreElementsS1 = false;
          buffer.setLength(0);
          charsetPossible=false;
          break;
        case '"':
        case '\'':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          buffer.append(c);
          currentState=STATE1INQUOTE;
          currentQuote=c;
          break;
        default:
          buffer.append(c);
        if(!isState1Present)
        {
          String s = buffer.toString().trim();
          if(!(s.equals("") || s.equals("/") || s.equals("<") || s.equals("<!") || s.equals("<!-") || s.equals("<!--")))
            currentState=STATE2;
        }
        if(logDEBUG) Logger.debug(this, "STATE1 default CASE: "+c);
        break;

        }
        break;

      case STATE1INQUOTE:
        if(logDEBUG) Logger.debug(this, "STATE1INQUOTE: "+c);
        switch(c)
        {
        case '"':
          if(currentQuote=='"' && prevc!='\\')
            currentState=STATE1;
          buffer.append(c);
          break;
        case '\'':
          if(currentQuote=='\'' && prevc!='\\')
            currentState=STATE1;
          buffer.append(c);
          break;
        case '\n':
          if(prevc == '\r') {
            break;
          }
          // Otherwise same as \r ...
        case '\f':
        case '\r':
          if(prevc != '\\') {
            ignoreElementsS1 = true;
            currentState = STATE1;
            break;
          } else {
            // Wipe out the \ as well.
            buffer.setLength(buffer.length()-1);
            break;
          }
        default:
          buffer.append(c);
        break;
        }
        break;


      case STATE2:
        canImport=false;
        charsetPossible=false;
        if(stopAtDetectedCharset)
          return;
        switch(c)
        {
        case '{':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }

          int i = 0;
          for(i=0;i<buffer.length();i++) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          if(logDEBUG) Logger.debug(this, "Appending whitespace in state2: \""+buffer.substring(0,i)+"\"");
          String ws = buffer.substring(0, i);
          buffer.delete(0, i);

          if(buffer.length() > 4 && buffer.substring(0, 4).equals("<!--")) {
            ws+=buffer.substring(0, 4);
            if(" \t\r\n".indexOf(buffer.charAt(4))==-1) {
              Logger.error(this, "<!-- not followed by whitespace!");
              return;
            }
            buffer.delete(0, 4);
            for(i=0;i<buffer.length();i++) {
              char c1 = buffer.charAt(i);
              if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
                continue;
              break;
            }
            ws+=buffer.substring(0, i);
            buffer.delete(0, i);
          }

          openBraces++;
          if(!buffer.toString().trim().equals(""))
          {
            String filtered=recursiveSelectorVerifier(buffer.toString());
            if(filtered!=null && !"".equals(filtered))
            {
              if(s2Comma)
              {
                filteredTokens.append(",");
                s2Comma=false;
              }
              filteredTokens.append(ws);
              filteredTokens.append(filtered);
              filteredTokens.append(" {");
            }
            else if(s2Comma && "".equals(filtered))
            {
              // There was a comma, so filteredTokens already contains some tokens.
              // The current selector is valid, yet banned. Ignore it.
              s2Comma=false;
              filteredTokens.append(ws);
              filteredTokens.append(" {");
            }
            else
            {
              ignoreElementsS2=true;
              // If there was a comma, filteredTokens may contain some tokens.
              // These are invalid, as per the spec: we wipe the whole selector out.
              // Also, not wiping filteredTokens here does bad things:
              // we would write the filtered tokens, without the { or }, so we end up prepending it to the next rule, which is not what we want as it changes the next rule's meaning.
              filteredTokens.setLength(0);
            }
            if(logDEBUG) Logger.debug(this, "STATE2 CASE { filtered elements"+filtered);
          } else {
            // No valid selector, wipe it out as above.
            ignoreElementsS2=true;
            // If there was a comma, filteredTokens may contain some tokens.
            // These are invalid, as per the spec: we wipe the whole selector out.
            // Also, not wiping filteredTokens here does bad things:
            // we would write the filtered tokens, without the { or }, so we end up prepending it to the next rule, which is not what we want as it changes the next rule's meaning.
            filteredTokens.setLength(0);
          }
          currentState=STATE3;
          openBracesStartingS3 = openBraces;
          if(logDEBUG) Logger.debug(this, "STATE2 -> STATE3, openBracesStartingS3 = "+openBracesStartingS3);
          buffer.setLength(0);
          break;

        case ',':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          for(i=0;i<buffer.length();i++) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          if(logDEBUG) Logger.debug(this, "Appending whitespace in state2: \""+buffer.substring(0,i)+"\"");
          ws = buffer.substring(0, i);
          buffer.delete(0, i);

          if(!s2Comma) {
            if(buffer.length() > 4 && buffer.substring(0, 4).equals("<!--")) {
              filteredTokens.append(buffer.substring(0, 4));
              if(" \t\r\n".indexOf(buffer.charAt(4))==-1) {
                Logger.error(this, "<!-- not followed by whitespace!");
                return;
              }
              buffer.delete(0, 4);
              for(i=0;i<buffer.length();i++) {
                char c1 = buffer.charAt(i);
                if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
                  continue;
                break;
              }
              filteredTokens.append(buffer.substring(0, i));
              buffer.delete(0, i);
            }
          }


          String filtered=recursiveSelectorVerifier(buffer.toString().trim());
          if(logDEBUG) Logger.debug(this, "STATE2 CASE , filtered elements"+filtered);
          if(filtered!=null && !"".equals(filtered))
          {
            if(s2Comma)
              filteredTokens.append(",");
            else
              s2Comma=true;
            filteredTokens.append(ws);
            filteredTokens.append(filtered);
          }
          else if("".equals(filtered))
          {
            // This selector was banned. Ignore it.
            filteredTokens.append(ws);
          }
          buffer.setLength(0);
          break;


        case '}':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          if(openBraces > 0 && !ignoreElementsS1) {
            openBraces--;
            // ignoreElementsS2 is irrelevant here, we are not *adding to* filteredTokens.
            if(openBraces >= 0)
              filteredTokens.append('}');
            else
              openBraces = 0;
            if(logDEBUG) Logger.debug(this, "Writing \""+filteredTokens+"\"");
            w.write(filteredTokens.toString());
          } else {
            if(openBraces > 0) openBraces--;
            // Ignore.
            // We are going back to STATE1, so reset ignoreElementsS1
            ignoreElementsS1 = false;
          }
          filteredTokens.setLength(0);
          buffer.setLength(0);
          currentMedia=new String[] {defaultMedia};
          isState1Present=false;
          currentState=STATE1;
          if(isInline) return;
          if(logDEBUG) Logger.debug(this, "STATE2 CASE }: "+c);
          break;

        case '"':
        case '\'':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          buffer.append(c);
          currentState=STATE2INQUOTE;
          currentQuote=c;
          break;

        default:
          buffer.append(c);
        if(logDEBUG) Logger.debug(this, "STATE2 default CASE: "+c);
        break;
        }
        break;

      case STATE2INQUOTE:
        if(logDEBUG) Logger.debug(this, "STATE2INQUOTE: "+c);
        charsetPossible=false;
        switch(c)
        {
        case '"':
          if(currentQuote=='"'&& prevc!='\\')
            currentState=STATE2;
          buffer.append(c);
          break;
        case '\'':
          if(currentQuote=='\''&& prevc!='\\')
            currentState=STATE2;
          buffer.append(c);
          break;
        case '\n':
          if(prevc == '\r') {
            break;
          }
          // Otherwise same as \r ...
        case '\f':
        case '\r':
          if(prevc != '\\') {
            ignoreElementsS2 = true;
            closeIgnoredS2 = true;
            currentState = STATE2;
            break;
          } else {
            // Wipe out the \ as well.
            buffer.setLength(buffer.length()-1);
            break;
          }
        default:
          buffer.append(c);
        break;
        }
        break;

      case STATE3:
        charsetPossible=false;
        if(stopAtDetectedCharset)
          return;
        switch(c)
        {
        case ':':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          if(openBraces > openBracesStartingS3) {
            // Correctly tokenise bogus properties containing {}'s, see CSS2.1 section 4.1.6.
            buffer.append(c);
            if(logDEBUG) Logger.debug(this, "openBraces now "+openBraces+" not moving on because openBracesStartingS3="+openBracesStartingS3+" in S3");
            break;
          }
          int i = 0;
          for(i=0;i<buffer.length();i++) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          if(logDEBUG) Logger.debug(this, "Appending whitespace: "+buffer.substring(0,i));
          whitespaceBeforeProperty = buffer.substring(0, i);
          propertyName=buffer.delete(0, i).toString().trim();
          if(logDEBUG) Logger.debug(this, "Property name: "+propertyName);
          buffer.setLength(0);
          if(logDEBUG) Logger.debug(this, "STATE3 CASE :: "+c);
          break;

        case ';':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          if(openBraces > openBracesStartingS3) {
            // Correctly tokenise bogus properties containing {}'s, see CSS2.1 section 4.1.6.
            buffer.append(c);
            if(logDEBUG) Logger.debug(this, "openBraces now "+openBraces+" not moving on because openBracesStartingS3="+openBracesStartingS3+" in S3");
            break;
          }

          i = 0;
          for(i=0;i<buffer.length();i++) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          if(logDEBUG) Logger.debug(this, "Appending whitespace after colon: \""+buffer.substring(0,i)+"\"");
          whitespaceAfterColon = buffer.substring(0, i);
          propertyValue=buffer.delete(0, i).toString().trim();
          if(logDEBUG) Logger.debug(this, "Property value: "+propertyValue);
          buffer.setLength(0);

          CSSPropertyVerifier obj=getVerifier(propertyName);
          if(obj != null) {
          ParsedWord[] words = split(propertyValue, obj.allowCommaDelimiters);
          if(logDEBUG) Logger.debug(this, "Split: "+CSSPropertyVerifier.toString(words));
          if(words != null && !ignoreElementsS2 && !ignoreElementsS3 && verifyToken(currentMedia,elements,obj,words))
          {
            if(changedAnything(words)) propertyValue = reconstruct(words);
            filteredTokens.append(whitespaceBeforeProperty);
            whitespaceBeforeProperty = "";
            filteredTokens.append(propertyName);
                                                filteredTokens.append(':');
                                                filteredTokens.append(whitespaceAfterColon);
                                                filteredTokens.append(propertyValue);
                                                filteredTokens.append(';');
            if(logDEBUG) Logger.debug(this, "STATE3 CASE ;: appending "+ propertyName+":"+propertyValue);
            if(logDEBUG) Logger.debug(this, "filtered tokens now: \""+filteredTokens.toString()+"\"");
          } else {
            if(logDEBUG) Logger.debug(this, "filtered tokens now (ignored): \""+filteredTokens.toString()+"\" words="+CSSPropertyVerifier.toString(words)+" ignoreS1="+ignoreElementsS1+" ignoreS2="+ignoreElementsS2+" ignoreS3="+ignoreElementsS3);
          }
          } else {
            if(logDEBUG) Logger.debug(this, "No such property name \""+propertyName+"\"");
          }
          ignoreElementsS3 = false;
          propertyName="";
          propertyValue="";
          break;
        case '}':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          openBraces--;
          if(openBraces > openBracesStartingS3-1) {
            // Correctly tokenise bogus properties containing {}'s, see CSS2.1 section 4.1.6.
            buffer.append(c);
            if(logDEBUG) Logger.debug(this, "openBraces now "+openBraces+" not moving on because openBracesStartingS3="+openBracesStartingS3+" in S3");
            if(openBraces < 0) openBraces = 0;
            break;
          }
          if(openBraces < 0) openBraces = 0;
          for(i=buffer.length()-1;i>=0;i--) {
            char c1 = buffer.charAt(i);
            if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
              continue;
            break;
          }
          i++;
          String postSpace = buffer.substring(i);
          buffer.setLength(i);
          // This (string!=) is okay as we set it directly by propertyName="" to indicate there is no property name.
          if(propertyName!="")
          {

            i = 0;
            for(i=0;i<buffer.length();i++) {
              char c1 = buffer.charAt(i);
              if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
                continue;
              break;
            }
            if(logDEBUG) Logger.debug(this, "Appending whitespace after colon (}): "+buffer.substring(0,i));
            whitespaceAfterColon = buffer.substring(0, i);
            buffer.delete(0, i);

            propertyValue=buffer.toString().trim();
            if(logDEBUG) Logger.debug(this, "Property value: "+propertyValue);
            buffer.setLength(0);

            obj=getVerifier(propertyName);
            if(logDEBUG) Logger.debug(this, "Found PropertyName:"+propertyName+" propertyValue:"+propertyValue);
            if(obj != null) {
              ParsedWord[] words = split(propertyValue,obj.allowCommaDelimiters);
              if(logDEBUG) Logger.debug(this, "Split: "+CSSPropertyVerifier.toString(words));
              if(!ignoreElementsS2 && !ignoreElementsS3 && verifyToken(currentMedia,elements,obj,words))
              {
                if(changedAnything(words)) propertyValue = reconstruct(words);
                filteredTokens.append(whitespaceBeforeProperty);
                whitespaceBeforeProperty = "";
                filteredTokens.append(propertyName);
                                                                filteredTokens.append(':');
                                                                filteredTokens.append(whitespaceAfterColon);
                                                                filteredTokens.append(propertyValue);
                if(logDEBUG) Logger.debug(this, "STATE3 CASE }: appending "+ propertyName+":"+propertyValue);
              }
            } else {
              if(logDEBUG) Logger.debug(this, "No such property name \""+propertyName+"\"");
            }
            propertyName="";
          } else {
            // Whitespace at end
            i = 0;
            for(i=0;i<buffer.length();i++) {
              char c1 = buffer.charAt(i);
              if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
                continue;
              break;
            }
            if(logDEBUG) Logger.debug(this, "Appending whitespace after colon (}): "+buffer.substring(0,i));
            filteredTokens.append(buffer.substring(0, i));
            buffer.delete(0, i);

          }
          ignoreElementsS3 = false;
          if((!ignoreElementsS2) || closeIgnoredS2) {
            filteredTokens.append(postSpace);
            filteredTokens.append("}");
            closeIgnoredS2 = false;
            ignoreElementsS2 = false;
          } else
            ignoreElementsS2=false;
          if(!ignoreElementsS1) {
            w.write(filteredTokens.toString());
            if(logDEBUG) Logger.debug(this, "writing filtered tokens: \""+filteredTokens.toString()+"\"");
          }
          filteredTokens.setLength(0);
          whitespaceAfterColon = "";
          if(forPage) {
            forPage = false;
            currentState = STATE1;
          } else {
            currentState=STATE2;
          }
          if(isInline) return;
          buffer.setLength(0);
          s2Comma=false;
          if(logDEBUG) Logger.debug(this, "STATE3 CASE }: "+c);
          break;

        case '{':
          // Correctly tokenise invalid properties including {}, see CSS2 section 4.1.6.
          openBraces++;
          buffer.append(c);
          if(logDEBUG) Logger.debug(this, "openBraces now "+openBraces+" in S3");
          break;
        case '"':
        case '\'':
          if(prevc == '\\') {
            // Leave in buffer, encoded.
            buffer.append(c);
            break;
          }
          buffer.append(c);
          currentState=STATE3INQUOTE;
          currentQuote=c;
          break;

        default:
          buffer.append(c);
        if(logDEBUG) Logger.debug(this, "STATE3 default CASE : "+c);
        break;

        }
        break;

      case STATE3INQUOTE:
        charsetPossible=false;
        if(stopAtDetectedCharset)
          return;
        if(logDEBUG) Logger.debug(this, "STATE3INQUOTE: "+c);
        switch(c)
        {
        case '"':
          if(currentQuote=='"'&& prevc!='\\')
            currentState=STATE3;
          buffer.append(c);
          break;
        case '\'':
          if(currentQuote=='\''&& prevc!='\\')
            currentState=STATE3;
          buffer.append(c);
          break;
        case '\n':
          if(prevc == '\r') {
            break;
          }
          // Otherwise same as \r ...
        case '\r':
        case '\f':
          if(prevc != '\\') {
            ignoreElementsS3 = true;
            currentState = STATE3;
            break;
          } else {
            // Wipe out the \ as well.
            buffer.setLength(buffer.length()-1);
            break;
          }
        default:
          buffer.append(c);
        break;
        }
        break;

      case STATECOMMENT:
        // FIXME sanitize (remove potentially dangerous chars) and preserve comments.
        charsetPossible=false;
        if(stopAtDetectedCharset)
          return;
        switch(c)
        {
        case '/':
          if(prevc=='*')
          {
            currentState=stateBeforeComment;
            c = 0;
            if(logDEBUG) Logger.debug(this, "Exiting the comment state "+currentState);
          }
          break;
        }
        break;
      }
    }

    if(logDEBUG) Logger.debug(this, "Filtered tokens: \""+filteredTokens+"\"");
    w.write(filteredTokens.toString());
    for(int i=0;i<openBraces;i++)
      w.write('}');

    if(logDEBUG) Logger.debug(this, "Remaining buffer: \""+buffer+"\"");

    int i = 0;
    for(i=0;i<buffer.length();i++) {
      char c1 = buffer.charAt(i);
      if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
        continue;
      break;
    }
    w.write(buffer.substring(0, i));
    buffer.delete(0, i);

    while(buffer.toString().trim().equals("-->")) {
      w.write("-->");
      buffer.delete(0, 3);
      for(i=0;i<buffer.length();i++) {
        char c1 = buffer.charAt(i);
        if(c1 == ' ' || c1 == '\f' || c1 == '\t' || c1 == '\r' || c1 == '\n')
          continue;
        break;
      }
      w.write(buffer.substring(0, i));
      buffer.delete(0, i);
    }

    // FIXME CSS2.1 section 4.2 "Unexpected end of style sheet".
    // We do NOT auto-close at the end.
    // It might be worth implementing this one day.

  }

  private String reconstruct(ParsedWord[] words) {
    StringBuilder sb = new StringBuilder();
    boolean first = true;
    ParsedWord lastWord = null;
    for(ParsedWord word : words) {
      if(lastWord != null && lastWord.postComma)
        sb.append(',');
      lastWord = word;
      if(!first) sb.append(" ");
      if(!word.changed) {
        sb.append(word.original);
        if(logDEBUG) Logger.debug(this, "Adding word (original): \""+word.original+"\"");
      } else {
        sb.append(word.encode(false)); // FIXME check if charset is full unicode, if so pass true
        if(logDEBUG) Logger.debug(this, "Adding word (new): \""+word.encode(false)+"\"");
      }
      first = false;
    }
    if(logDEBUG) Logger.debug(this, "Reconstructed: \""+sb.toString()+"\"");
    return sb.toString();
  }


  private boolean changedAnything(ParsedWord[] words) {
    for(ParsedWord word : words) {
      if(word.changed) return true;
    }
    return false;
  }


  private ArrayList<String> commaListFromIdentifiers(ParsedWord[] strparts, int offset) {
    ArrayList<String> medias = new ArrayList<String>(strparts.length-1);
    if(strparts.length <= offset) {
      // Nothing to munch
    } else if(strparts.length == offset+1 && strparts[1] instanceof ParsedIdentifier) {
      medias.add(((ParsedIdentifier)strparts[1]).getDecoded());
    } else {
      boolean first = true;
      for(ParsedWord word : strparts) {
        if(first) {
          first = false;
          continue;
        }
        if(word instanceof ParsedIdentifier) {
          medias.add(((ParsedIdentifier)word).getDecoded());
        } else if(word instanceof SimpleParsedWord) {
          String data = ((SimpleParsedWord)word).original;
          String[] split = FilterUtils.removeWhiteSpace(data.split(","),false);
                                        medias.addAll(Arrays.asList(split));
        } else return null;
      }
    }
    return medias;
  }

  static abstract class ParsedWord {

    final String original;
    /** Has decoded changed? If not we can use the original. */
    protected boolean changed;
    public boolean postComma;

    public ParsedWord(String original, boolean changed) {
      this.original = original;
      this.changed = changed;
    }

    public String encode(boolean unicode) {
      if(!changed)
        return original;
      else {
        StringBuilder out = new StringBuilder();
        innerEncode(unicode, out);
        return out.toString();
      }
    }

                @Override
    public String toString() {
      return super.toString()+":\""+original+"\"";
    }

    abstract protected void innerEncode(boolean unicode, StringBuilder out);

  }

  /** Note that this does not represent functionThingy's.
   * counter() for example can contain a string, it is difficult to
   * determine whether to encode strings if we don't know they are strings!
   * This only handles keywords and strings.
   */
  static abstract class BaseParsedWord extends ParsedWord {
    private String decoded;

    /**
     * @param original
     * @param decoded
     * @param changed Set this to true if we don't like the original
     * encoding and have changed something during decode.
     */
    BaseParsedWord(String original, String decoded, boolean changed) {
      super(original, changed);
      this.decoded = decoded;
    }

    @Override
    protected void innerEncode(boolean unicode, StringBuilder out) {
      char prevc = 0;
      char c = 0;
      for(int i=0;i<decoded.length();i++) {
        prevc = c;
        c = decoded.charAt(i);
        if(!mustEncode(c, i, prevc, unicode)) {
          out.append(c);
        } else {
          encodeChar(c, out);
        }
      }
    }

    abstract protected boolean mustEncode(char c, int i, char prevc, boolean unicode);

    private void encodeChar(char c, StringBuilder sb) {
      String s = Integer.toHexString(c);
      sb.append('\\');
      if(s.length() == 6)
        sb.append(s);
      else if(s.length() > 6)
        throw new IllegalStateException();
      else {
        int x = 6 - s.length();
        for(int i=0;i<x;i++)
          sb.append('0');
        sb.append(s);
      }
    }

    public String getDecoded() {
      return decoded;
    }

    public void setNewValue(String s) {
      this.changed = true;
      this.decoded = s;
    }

  }

  static class ParsedIdentifier extends BaseParsedWord {

    ParsedIdentifier(String original, String decoded, boolean changed) {
      super(original, decoded, changed);
    }

    @Override
    protected boolean mustEncode(char c, int i, char prevc, boolean unicode) {
      // It is an identifier.
      if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
          (c >= '0' && c <= '9') || c == '-' || c == '_'
            || (c >= (char)0x00A1 && unicode)) {
        // Cannot start with a digit or a hyphen followed by a digit.
        if(!((i == 0 && (c >= '0' && c <= '9')) ||
            (i == 1 && prevc == '-' &&
                (c >= '0' && c <= '9'))))
          return false;
      }
      return true;
    }

  }

  static class ParsedString extends BaseParsedWord {

    ParsedString(String original, String decoded, boolean changed, char stringChar) {
      super(original, decoded, changed);
      this.stringChar = stringChar;
    }

    /** Is the word quoted? If true, the word is completely enclosed
     * by the given string character (either ' or "), which are not
     * included in the decoded string. */
    final char stringChar;

    @Override
    protected boolean mustEncode(char c, int i, char prevc, boolean unicode) {
      // It is a string.
      // Anything is allowed in a string...
      if(c == '\r' || c == '\n' || c == '\f')
        // Except newlines.
        return true;
      else if(c == stringChar)
        // And the quote itself.
        return true;
      else if(c < 32 || (c >= (char)0x0080 && !unicode))
        // And control chars, and anything outside Basic Latin (unless we know the output charset is unicode-complete).
        return true;
      return false;
    }

                @Override
    protected void innerEncode(boolean unicode, StringBuilder out) {
      out.append(stringChar);
      super.innerEncode(unicode, out);
      out.append(stringChar);
    }

  }

  static class ParsedURL extends ParsedString {

    ParsedURL(String original, String decoded, boolean changed, char stringChar) {
      super(original, decoded, changed || stringChar == 0, stringChar == 0 ? '"' : stringChar);
    }

                @Override
    protected void innerEncode(boolean unicode, StringBuilder out) {
      out.append("url(");
      super.innerEncode(unicode, out);
      out.append(')');
    }

    public void setNewURL(String s) {
      super.setNewValue(s);
    }

  }

  static class ParsedAttr extends ParsedIdentifier {

    ParsedAttr(String original, String decoded, boolean changed) {
      super(original, decoded, changed);
    }

                @Override
    protected void innerEncode(boolean unicode, StringBuilder out) {
      out.append("attr(");
      super.innerEncode(unicode, out);
      out.append(')');
    }

  }

  /** Simple parsed word, doesn't need encoding, won't be changed. Used
   * for lengths, percentages, angles, etc. All characters must be safe
   * and non-problematic. This is used for everything from lengths and
   * percentages to rgb(...) with spaces in it. Anything we don't
   * understand gets a SimpleParsedWord.
   * */
  static class SimpleParsedWord extends ParsedWord {

    public SimpleParsedWord(String original) {
      super(original, false);
    }

    @Override
    protected void innerEncode(boolean unicode, StringBuilder out) {
      out.append(original);
    }

  }

  /** Counters need special handling, partly because they contain
   * attributes and strings. */
  static class ParsedCounter extends ParsedWord {
    public ParsedCounter(String original, ParsedIdentifier identifier, ParsedIdentifier listType, ParsedString separatorString) {
      super(original, true);
      this.identifier = identifier;
      this.listType = listType;
      this.separatorString = separatorString;
    }

    private final ParsedIdentifier identifier;
    private final ParsedIdentifier listType;
    private final ParsedString separatorString;

    @Override
    protected void innerEncode(boolean unicode, StringBuilder out) {
      if(separatorString != null)
        out.append("counters(");
      else
        out.append("counter(");
      identifier.innerEncode(unicode, out);
      if(separatorString != null) {
        out.append(", ");
        separatorString.innerEncode(unicode, out);
      }
      if(listType != null) {
        out.append(", ");
        listType.innerEncode(unicode, out);
      }
      out.append(')');
      if(postComma) out.append(',');
    }

    protected boolean addComma() {
      return false;
    }

  }

  /** Split up a string, taking into account CSS rules for escaping,
   * strings, identifiers.
   * @param str1
   * @return
   */
  private static ParsedWord[] split(String input, boolean allowCommaDelimiters) {
    if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "Splitting \""+input+"\" allowCommaDelimiters="+allowCommaDelimiters);
    ArrayList<ParsedWord> words = new ArrayList<ParsedWord>();
    ParsedWord lastWord = null;
    char c = 0;
    // ", ' or 0 (not in string)
    char stringchar = 0;
    boolean escaping = false;
    /** Eat the next linefeed due to an escape closing. We turned the
     * \r into a space, so we just ignore the \n. */
    boolean eatLF = false;
    /** The original token */
    StringBuilder origToken = new StringBuilder(input.length());
    /** The decoded token */
    StringBuilder decodedToken = new StringBuilder(input.length());
    /** We don't like the original token, it bends the spec in unacceptable ways */
    boolean dontLikeOrigToken = false;
    StringBuilder escape = new StringBuilder(6);
    boolean couldBeIdentifier = true;
    boolean addComma = false;
    // Brackets prevent tokenisation, see e.g. rgb().
    int bracketCount = 0;
    for(int i=0;i<input.length();i++) {
      c = input.charAt(i);
      if(stringchar == 0) {
        if(eatLF && c == '\n') {
          eatLF = false;
          continue;
        } else
          eatLF = false;
        // Not in a string
        if(!escaping) {
          if((" \t\r\n\f".indexOf(c) != -1 || (allowCommaDelimiters && c == ',')) && bracketCount == 0) {
            if(c == ',') {
              if(decodedToken.length() == 0) {
                if(lastWord == null) {
                  if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "Extra comma before first element in \""+input+"\" i="+i);
                  return null;
                } else if(lastWord.postComma) {
                  if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "Extra comma after element "+lastWord+" in \""+input+"\" i="+i);
                  // Allow it, delete it.
                  lastWord.changed = true;
                } else
                  lastWord.postComma = true;
              // Comma is not added to the buffer, so this works even for element , element
              } else {
                if(addComma) {
                  if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "Extra comma after a comma in \""+input+"\" i="+i);
                  return null;
                }
                addComma = true;
              }
            }
            // Legal CSS whitespace
            if(decodedToken.length() > 0) {
              ParsedWord word = parseToken(origToken, decodedToken, dontLikeOrigToken, couldBeIdentifier);
              if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "Token: orig: \""+origToken.toString()+"\" decoded: \""+decodedToken.toString()+"\" dontLike="+dontLikeOrigToken+" couldBeIdentifier="+couldBeIdentifier+" parsed "+word);
              if(word == null) return null;
              if(addComma) {
                word.postComma = true;
                addComma = false;
              }
              words.add(word);
              origToken.setLength(0);
              decodedToken.setLength(0);
              dontLikeOrigToken = false;
              couldBeIdentifier = true;
              lastWord = word;
            } // Else ignore.
          } else if(c == '\"') {
            stringchar = c;
            origToken.append(c);
            decodedToken.append(c);
            couldBeIdentifier = false;
          } else if(c == '\'') {
            stringchar = c;
            origToken.append(c);
            decodedToken.append(c);
            couldBeIdentifier = false;
          } else if(c == '\\') {
            origToken.append(c);
            escape.setLength(0);
            escaping = true;
          } else if(c == '(') {
            bracketCount++;
            origToken.append(c);
            decodedToken.append(c);
            couldBeIdentifier = false;
          } else if(c == ')') {
            bracketCount--;
            if(bracketCount < 0)
              return null;
            origToken.append(c);
            decodedToken.append(c);
            couldBeIdentifier = false;
          } else {
            if(couldBeIdentifier) {
              if(!((c >= '0' && c <= '9' && origToken.length() > 0) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_' || c >= 0xA1))
                couldBeIdentifier = false;
              if(origToken.length() == 1 && origToken.charAt(0) == '-' && (c >= '0' && c <= '9'))
                couldBeIdentifier = false;
            }
            origToken.append(c);
            decodedToken.append(c);
          }
        } else if(escaping && escape.length() == 0) {
          if(c == '\"' || c == '\'') {
            escaping = false;
            origToken.append(c);
            decodedToken.append(c);
          } else if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
            escape.append(c);
          } else if(c == '\n' || c == '\r' || c == '\f') {
            // Newline. Can only be escaped in a string.
            // Not valid so return null.
            return null;
          } else {
            escaping = false;
            origToken.append(c);
            decodedToken.append(c);
          }
        } else /*if(escaping && escape.length() != 0)*/ {
          if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
            escape.append(c);
            if(escape.length() == 6) {
              origToken.append(escape);
              decodedToken.append((char)Integer.parseInt(escape.toString(), 16));
              escape.setLength(0);
              escaping = false;
            }
          } else if(" \t\r\n\f".indexOf(c) != -1) {
            // Whitespace other than CR terminates the escape without any further significance.
            origToken.append(escape);
            decodedToken.append((char)Integer.parseInt(escape.toString(), 16));
            // Convert it to standard whitespace to avoid any complications.
            origToken.append(" ");
            escape.setLength(0);
            escaping = false;
            // \r terminates the escape but might be followed by a \n
            if(c == '\r')
              eatLF = true;
          } else {
            // Already started the escape, anything other than a hex digit or whitespace is invalid.
            return null;
          }
        }
      } else {
        // We are in a string.

        if(eatLF && c == '\n') {
          // Slightly different meaning here.
          // Here we do want to include it in the string.
          eatLF = false;
          origToken.append(c);
          // Don't add to decoded because it is invisible along with the preceding \
          continue;
        } else
          eatLF = false;

        if(c == stringchar && !escaping) {
          origToken.append(c);
          decodedToken.append(c);
          stringchar = 0;
        } else if(c == '\f' || c == '\r' || c == '\n' && !escaping) {
          // Invalid end of line in string.
          // The whole construct is invalid.
          // That is, usually everything up to the next semicolon.
          return null;
        } else if(c == '\\' && !escaping) {
          escaping = true;
          escape.setLength(0);
          origToken.append(c);
        } else if(escaping && escape.length() == 0) {
          if(c == '\"' || c == '\'') {
            escaping = false;
            origToken.append(c);
            decodedToken.append(c);
          } else if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
            escape.append(c);
          } else if(c == '\r' || c == '\n' || c == '\f') {
            // In a string, an escaped newline is equal to nothing.
            origToken.append(c);
            // Do not add to decodedToken because both the \ and the \n are ignored.
            // Eat the \n if necessary (copy it to the origToken but not the decodedToken)
            if(c == '\r') eatLF = true;
          } else {
            origToken.append(c);
            decodedToken.append(c);
            // Escape one character.
            escaping = false;
          }
        } else if(escaping/* && escape.length() > 0*/) {
          if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
            escape.append(c);
            if(escape.length() == 6) {
              origToken.append(escape);
              decodedToken.append((char)Integer.parseInt(escape.toString(), 16));
              escape.setLength(0);
              escaping = false;
            }
          } else if(" \t\r\n\f".indexOf(c) != -1) {
            // Whitespace other than CR terminates the escape without any further significance.
            origToken.append(escape);
            decodedToken.append((char)Integer.parseInt(escape.toString(), 16));
            escape.setLength(0);
            escaping = false;
            // \r terminates the escape but might be followed by a \n
            if(c == '\r') {
              eatLF = true;
              // Messy...
              dontLikeOrigToken = true;
            }
          } else {
            // Already started the escape, anything other than a hex digit or whitespace is invalid.
            return null;
          }
        } else { // escaping = false
          origToken.append(c);
          decodedToken.append(c);
        }
      }

    }
    if(escaping && escape.length() > 0) {
      origToken.append(escape);
      decodedToken.append((char)Integer.parseInt(escape.toString(), 16));
    } else if(escaping) {
      // Newline rule?
      dontLikeOrigToken = true;
    }
    if(origToken.length() > 0) {
      if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "Token: orig: \""+origToken.toString()+"\" decoded: \""+decodedToken.toString()+"\" dontLike="+dontLikeOrigToken+" couldBeIdentifier="+couldBeIdentifier);
      ParsedWord word = parseToken(origToken, decodedToken, dontLikeOrigToken, couldBeIdentifier);
      if(word == null) return null;
      words.add(word);
    }
    return words.toArray(new ParsedWord[words.size()]);
  }


  private static ParsedWord parseToken(StringBuilder origToken, StringBuilder decodedToken, boolean dontLikeOrigToken, boolean couldBeIdentifier) {
    if(origToken.length() > 2) {
      char c = origToken.charAt(0);
      if(c == '\'' || c == '\"') {
        char d = origToken.charAt(origToken.length()-1);
        if(c == d) {
          // The word is a string.
          decodedToken.setLength(decodedToken.length()-1);
          decodedToken.deleteCharAt(0);
          return new ParsedString(origToken.toString(), decodedToken.toString(), dontLikeOrigToken, c);
        } else {
          if(d != ',') {
            // No whitespace after a string...
            return null;
          } else return new SimpleParsedWord(origToken.toString());
        }
      }
    }

    String s = origToken.toString();
    if(couldBeIdentifier)
      return new ParsedIdentifier(s, decodedToken.toString(), dontLikeOrigToken);

    String sl = s.toLowerCase();
    if(sl.startsWith("url(")) {
      if(s.endsWith(")")) {
        decodedToken.delete(0, 4);
        decodedToken.setLength(decodedToken.length()-1);
        if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "stripped: "+decodedToken);

        // Trim whitespace from both ends

        String strippedOrig = s.substring(4, s.length()-1);
        int i;
        for(i=0;i<strippedOrig.length();i++) {
          char c = strippedOrig.charAt(i);
          if(!(c == ' ' || c == '\t')) break;
        }
        decodedToken.delete(0, i);
        strippedOrig = strippedOrig.substring(i);
        for(i=strippedOrig.length()-1;i>=0;i--) {
          char c = strippedOrig.charAt(i);
          if(!(c == ' ' || c == '\t')) break;
          if(i > 0 && strippedOrig.charAt(i-1) == '\\') break;
        }
        decodedToken.setLength(decodedToken.length()-(strippedOrig.length()-i-1));
        strippedOrig = strippedOrig.substring(0, i+1);

        if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "whitespace stripped: "+strippedOrig+" decoded "+decodedToken);

        if(strippedOrig.length() == 0) return null;

        if(strippedOrig.length() > 2) {
          char c = strippedOrig.charAt(0);
          if(c == '\'' || c == '\"') {
            char d = strippedOrig.charAt(strippedOrig.length()-1);
            if(c == d) {
              // The word is a string.
              decodedToken.setLength(decodedToken.length()-1);
              decodedToken.deleteCharAt(0);
              if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "creating url(): orig=\""+origToken.toString()+"\" decoded=\""+decodedToken.toString()+"\"");
              return new ParsedURL(origToken.toString(), decodedToken.toString(), dontLikeOrigToken, c);
            } else
              return null;
          }
        }
        return new ParsedURL(origToken.toString(), decodedToken.toString(), dontLikeOrigToken, (char)0);
      } else return null;
    }

    if(sl.startsWith("attr(")) {
      if(s.endsWith(")")) {
        decodedToken.delete(0, 5);
        decodedToken.setLength(decodedToken.length()-1);

        // Trim whitespace from both ends

        String strippedOrig = s.substring(4, s.length()-1);
        int i;
        for(i=0;i<strippedOrig.length();i++) {
          char c = strippedOrig.charAt(i);
          if(!(c == ' ' || c == '\t')) break;
        }
        decodedToken.delete(0, i);
        strippedOrig = strippedOrig.substring(i);
        for(i=strippedOrig.length()-1;i>=0;i--) {
          char c = strippedOrig.charAt(i);
          if(!(c == ' ' || c == '\t')) break;
          if(i > 0 && strippedOrig.charAt(i-1) == '\\') break;
        }
        decodedToken.setLength(decodedToken.length()-(strippedOrig.length()-i-1));
        strippedOrig = strippedOrig.substring(0, i+1);

        if(strippedOrig.length() == 0) return null;

        return new ParsedAttr(origToken.toString(), decodedToken.toString(), dontLikeOrigToken);
      } else return null;
    }

    boolean plural = false;
    if(sl.startsWith("counter(") || (plural = sl.startsWith("counters("))) {
      if(s.endsWith(")")) {
        int len = plural ? "counters(".length() : "counter(".length();
        decodedToken.delete(0, len);
        decodedToken.setLength(decodedToken.length()-1);

        // Trim whitespace from both ends

        String strippedOrig = s.substring(len, s.length()-1);
        int i;
        for(i=0;i<strippedOrig.length();i++) {
          char c = strippedOrig.charAt(i);
          if(!(c == ' ' || c == '\t')) break;
        }
        decodedToken.delete(0, i);
        strippedOrig = strippedOrig.substring(i);
        for(i=strippedOrig.length()-1;i>=0;i--) {
          char c = strippedOrig.charAt(i);
          if(!(c == ' ' || c == '\t')) break;
          if(i > 0 && strippedOrig.charAt(i-1) == '\\') break;
        }
        decodedToken.setLength(decodedToken.length()-(strippedOrig.length()-i-1));
        strippedOrig = strippedOrig.substring(0, i+1);

        if(strippedOrig.length() == 0) return null;

        String[] split = FilterUtils.removeWhiteSpace(strippedOrig.split(","),false);
        if(split.length == 0 || (plural && split.length > 3) || ((!plural) && split.length > 2) || (plural && split.length < 2))
          return null;

        ParsedIdentifier ident = makeParsedIdentifier(split[0]);
        if(ident == null) return null;
        ParsedString separator = null;
        ParsedIdentifier listType = null;
        if(plural) {
          separator = makeParsedString(split[1]);
          if(separator == null) return null;
        }
        if(((!plural) && split.length == 2) || (plural && split.length == 3)) {
          listType = makeParsedIdentifier(split[split.length-1]);
          if(listType == null) return null;
        }
        return new ParsedCounter(origToken.toString(), ident, listType, separator);
      } else return null;
    }

    return new SimpleParsedWord(origToken.toString());
  }

  private static ParsedIdentifier makeParsedIdentifier(String string) {
    ParsedWord[] words = split(string,false);
    if(words == null) return null;
    if(words.length != 1) return null;
    if(!(words[0] instanceof ParsedIdentifier)) return null;
    return (ParsedIdentifier)words[0];
  }

  private static ParsedString makeParsedString(String string) {
    ParsedWord[] words = split(string,false);
    if(words == null) return null;
    if(words.length != 1) return null;
    if(!(words[0] instanceof ParsedString)) return null;
    return (ParsedString)words[0];
  }

  /*
   * Basic class to verify value for a CSS Property. This class can verify values which are
   * Integer,Real,Percentage, <Length>, <Angle>, <Color>, <URI>, <Shape> and so on.
   * parserExpression is used for verifying regular expression for Property value
   * e.g. [ <color> | transparent]{1,4}.
   */
  static class CSSPropertyVerifier
  {
    public final boolean onlyValueVerifier;
    public final boolean allowCommaDelimiters;
   
    public final Set<String> allowedValues; //immutable HashSet for all String constants that this CSS property can assume like "inherit"
    public final Set<String> allowedMedia; // immutable HashSet for all valid Media for this CSS property.

    /*
     * in, re etc stands for different code strings using which these boolean values can be set in
     * constructor like passing in,re would set isInteger and isReal.
     */
    public final boolean isInteger;    //in
    public final boolean isReal;       //re
    public final boolean isPercentage; //pe
    public final boolean isLength;     //le
    public final boolean isAngle;      //an
    public final boolean isColor;      //co
    public final boolean isURI;        //ur
    public final boolean isShape;      //sh
    public final boolean isString;     //st
    public final boolean isCounter;    //co
    public final boolean isIdentifier; //id
    public final boolean isTime;       //ti
    public final boolean isFrequency;  //fr
    public final boolean isTransform;  //tr
   
    private final List<String> parserExpressions;
   
    CSSPropertyVerifier(boolean allowCommaDelimiters)
    {
      this(null, null, null, null, false, allowCommaDelimiters);
    }

    CSSPropertyVerifier(Collection<String> allowedValues,
                        Collection<String> allowedMedia)
    {
      this(allowedValues, allowedMedia, null, null);
    }

    CSSPropertyVerifier(Collection<String> allowedValues,
                        Collection<String> allowedMedia,
                        Collection<String> possibleValues)
    {
      this(allowedValues, allowedMedia, possibleValues, null);
    }

    CSSPropertyVerifier(Collection<String> allowedValues,
                        Collection<String> possibleValues,
                        Collection<String> parseExpression,
                        Collection<String> allowedMedia,
                        boolean onlyValueVerifier)
    {
      this(allowedValues, allowedMedia, possibleValues, parseExpression, onlyValueVerifier, false);
    }

    CSSPropertyVerifier(Collection<String> allowedValues,
                        Collection<String> allowedMedia,
                        Collection<String> possibleValues,
                        Collection<String> parseExpression)
    {
      this(allowedValues, allowedMedia, possibleValues, parseExpression, false, false);
    }

    CSSPropertyVerifier(Collection<String> allowedValues,
                        Collection<String> allowedMedia,
                        Collection<String> possibleValues,
                        Collection<String> parseExpression,
                        boolean onlyValueVerifier,
                        boolean allowCommaDelimiters)
    {
      this.onlyValueVerifier = onlyValueVerifier;
      this.allowCommaDelimiters = allowCommaDelimiters;

      boolean isInteger, isReal, isPercentage, isLength, isAngle, isColor,
        isURI, isShape, isString, isCounter, isIdentifier, isTime,
        isFrequency, isTransform;
      isInteger = isReal = isPercentage = isLength = isAngle = isColor = isURI
        = isShape = isString = isCounter = isIdentifier = isTime
        = isFrequency = isTransform = false;
      if(possibleValues != null) {
        for(String possibleValue : possibleValues) {
          if("in".equals(possibleValue))
            isInteger=true; //in
          else if("re".equals(possibleValue))
            isReal=true//re
          else if("pe".equals(possibleValue))
            isPercentage=true//pe
          else if("le".equals(possibleValue))
            isLength=true//le
          else if("an".equals(possibleValue))
            isAngle=true//an
          else if("co".equals(possibleValue))
            isColor=true; //co
          else if("ur".equals(possibleValue))
            isURI=true//ur
          else if("sh".equals(possibleValue))
            isShape=true//sh
          else if("st".equals(possibleValue))
            isString=true;//st
          else if("co".equals(possibleValue))
            isCounter=true; //co
          else if("id".equals(possibleValue))
            isIdentifier=true; //id
          else if("ti".equals(possibleValue))
            isTime=true; //ti
          else if("fr".equals(possibleValue))
            isFrequency=true; //fr
          else if("tr".equals(possibleValue))
            isTransform=true; //tr
        }
      }
      this.isInteger = isInteger;
      this.isReal = isReal;
      this.isPercentage = isPercentage;
      this.isLength = isLength;
      this.isAngle = isAngle;
      this.isColor = isColor;
      this.isURI = isURI;
      this.isShape = isShape;
      this.isString = isString;
      this.isCounter = isCounter;
      this.isIdentifier = isIdentifier;
      this.isTime = isTime;
      this.isFrequency = isFrequency;
      this.isTransform = isTransform;
     
      if (allowedValues != null) {
        this.allowedValues = Collections.unmodifiableSet(new HashSet<String>(allowedValues));
      } else {
        this.allowedValues = null;
      }

      if (allowedMedia != null) {
        this.allowedMedia = Collections.unmodifiableSet(new HashSet<String>(allowedMedia));
      } else {
        this.allowedMedia = null;
      }

      if (parseExpression != null) {
        this.parserExpressions = Collections.unmodifiableList(new ArrayList<String>(parseExpression));
      } else {
        this.parserExpressions = Collections.emptyList();
      }
    }

    public static boolean isIntegerChecker(String value)
    {
      try{
        Integer.parseInt(value); //CSS Property has a valid integer.
        return true;
      }
      catch(Exception e) {return false; }

    }


    public static boolean isRealChecker(String value)
    {
      try
      {
        Float.parseFloat(value); //Valid float
        return true;
      }
      catch(Exception e){return false; }
    }

    public static boolean isValidURI(ParsedURL word, FilterCallback cb)
    {
      String w = CSSTokenizerFilter.removeOuterQuotes(word.getDecoded());
      //if(debug) Logger.debug(this, "CSSPropertyVerifier isVaildURI called cb="+cb);
      try
      {
        //if(debug) Logger.debug(this, "CSSPropertyVerifier isVaildURI "+cb.processURI(URI, null));
        String s = cb.processURI(w, null);
        if(s == null || s.equals("")) return false;
        if(s.equals(w)) return true;
        if(logDEBUG) Logger.debug(CSSTokenizerFilter.class, "New url: \""+s+"\" from \""+w+"\"");
        word.setNewURL(s);
        return true;
      }
      catch(CommentException e)
      {
        //if(debug) Logger.debug(this, "CSSPropertyVerifier isVaildURI Exception"+e.toString());
        return false;
      }

    }

    public boolean checkValidity(ParsedWord[] words, FilterCallback cb)
    {
      return this.checkValidity(null,null, words, cb);
    }

    public boolean checkValidity(ParsedWord word, FilterCallback cb)
    {
      return this.checkValidity(null,null, new ParsedWord[] { word }, cb);
    }

    // Verifies whether this CSS property can have a value under given media and HTML elements
    public boolean checkValidity(String[] media,String[] elements,ParsedWord[] words, FilterCallback cb)
    {

      if(logDEBUG) Logger.debug(this, "checkValidity for "+toString(words)+" for "+this);
      if(!onlyValueVerifier)
      {
        if(allowedMedia!=null) {
          boolean allowed = false;
          for(String m : media)
            if(allowedMedia.contains(m)) {
              allowed = true;
              break;
            }
          if(!allowed) {
            if(logDEBUG) Logger.debug(this, "checkValidity Media of the element is not allowed.Media="+Fields.commaList(media)+" allowed Media="+allowedMedia.toString());

            return false;
          }
        }
        // CONFORMANCE: ELEMENT CHECKING DISABLED:
        // We do NOT check whether a property is allowed for a specific element.
        // According to the spec, all properties are defined for all elements,
        // and in many cases they will be set on one element to which they do
        // not apply, and then be inherited to other elements for which they do apply.
        // FOR EXAMPLE, IN THE SPEC:
        // 17.6.1.1 empty-cells Applies to: 'table-cell' elements
        // In 17.2, 'table-cell' is explicitly defined:
        // table-cell (In HTML: TD, TH)     Specifies that an element represents a table cell.
        // Yet the example for empty-cells is:
        // The following rule causes borders and backgrounds to be drawn around all cells:
        // table { empty-cells: show }
        // Hence it is not appropriate to check which property is valid for which element.

        // In security terms, there is no danger in allowing every property on every element.
      }

      if(words.length == 1) {

      if(words[0] instanceof ParsedIdentifier && allowedValues != null && allowedValues.contains(((ParsedIdentifier)words[0]).original.toLowerCase()))
      //CSS Property has one of the explicitly defined values
        return true;

      if(words[0] instanceof SimpleParsedWord) {

        String word = ((SimpleParsedWord)words[0]).original;

        // Numeric explicitly defined value is possible
        if(allowedValues != null && allowedValues.contains(word))
          return true;

        // These are all numeric so they will have parsed as a SimpleParsedWord.

        if(isInteger && isIntegerChecker(word))
        {
          return true;
        }

        if(isReal && isRealChecker(word))
        {
          return true;
        }

        if(isPercentage && FilterUtils.isPercentage(word)) //Valid percentage X%
        {
          return true;
        }

        if(isLength && FilterUtils.isLength(word,false)) //Valid unit Vxx where xx is unit or V
        {
          return true;
        }

        if(isAngle && FilterUtils.isAngle(word))
        {
          return true;
        }
        // This is not numeric but will still have parsed as a SimpleParsedWord, as it either starts with a # or has brackets in.
        if(isColor)
        {
          if(FilterUtils.isColor(word))
            return true;
        }

        if(isShape)
        {
          if(FilterUtils.isValidCSSShape(word))
            return true;
        }

        if(isFrequency)
        {
          if(FilterUtils.isFrequency(word))
            return true;
        }

        if(isTime) {
          if(FilterUtils.isTime(word))
            return true;
        }

        if(isTransform) {
          if(FilterUtils.isCSSTransform(word))
            return true;
        }
      }

      if(words[0] instanceof ParsedIdentifier && isColor) {
        if(FilterUtils.isColor(((ParsedIdentifier)words[0]).original))
          return true;

      }
      if(isURI && words[0] instanceof ParsedURL)

      {
        return isValidURI((ParsedURL)words[0], cb);
      }

      if(isIdentifier && words[0] instanceof ParsedIdentifier)
      {
        return true;
      }

      if(isString && words[0] instanceof ParsedString)
      {
        if(ElementInfo.ALLOW_ALL_VALID_STRINGS || ElementInfo.isValidStringDecoded(((ParsedString)words[0]).getDecoded()))
          return true;
        else
          return false;
      }

      }

      /*
       * Parser expressions
       * 1 || 2 => 1a2
       * [1][2] =>1 2
       * 1<1,4> => 1<1,4>
       * 1* => 1<0,65536>
       * 1? => 1o
       *
       */
      /*
       * For each parserExpression, recursiveParserExpressionVerifier() would be called with parserExpression and value.
       */
      for(String parserExpression : parserExpressions)
      {
        boolean result=recursiveParserExpressionVerifier(parserExpression,words,cb);

        if(result)
          return true;
      }
      return false;
    }
    /* parserExpression string would be interpreted as 1 || 2 => 1a2 here 1a2 would be written to parse 1 || 2 where 1 and 2 are auxilaryVerifiers[1] and auxilaryVerifiers[2] respectively i.e. indices in auxilaryVerifiers
     * [1][2] =>1b2
     * 1<1,4> => 1<1,4>
     * 1* => 1<0,65536>
     * 1? => 1o
     * 1+=>1<1,65536>
     * Additional expressions that can be passed to the function
     * 1 2 => both 1 and 2 should return true where 1 and 2 are again indices in auxiliaryVerifier array.
     * [a,b]=> give at least a tokens and at the most b tokens(part of values) to this block of expression.
     * e.g. 1[2,3] and the value is "hello world program" then object 1 would be tested with "hello world"
     * and "hello world program".
     * The main logic of this function is find a set of values for different part of the ParserExpression so that each part returns true.
     * e.g. Suppose the expression is
     * (1 || 2) 3
     * where object 1 can consume upto 2 tokens, 2 can consume upto 2 tokens and 3 would consume one and only one token.
     * This expression would be encoded as
     * "1a2" for 1 || 2 Using this, third object would be created say 4 e.g. 4="1a2"
     * Now the main object would be given the parserExpression as
     * "4<0,4> 3"
     * This function would call
     * 4 with 0 tokens and 3 with the remaining
     * 4 with 1 tokens and 3 with the remaining
     * and so on.
     * If all combinations are failed then it would return false. If any combination gives true value
     * then return value would be true.
     */
    public boolean recursiveParserExpressionVerifier(String expression,ParsedWord[] words, FilterCallback cb)
    {
      if(logDEBUG) Logger.debug(this, "1recursiveParserExpressionVerifier called: with "+expression+" "+toString(words));
      if((expression==null || ("".equals(expression.trim()))))
      {
        if(words==null || words.length == 0)
          return true;
        else
          return false;
      }

      int tokensCanBeGivenLowerLimit=1,tokensCanBeGivenUpperLimit=1;
      for(int i=0;i<expression.length();i++)
      {
        if(expression.charAt(i)=='a') //Identifying ||
        {
          int noOfa=0;
          int endIndex=expression.length();
          //Detecting the other end
          for(int j=0;j<expression.length();j++)
          {
            if(expression.charAt(j)=='?' || expression.charAt(j)=='<' || expression.charAt(j)=='>' || expression.charAt(j)==' ')
            {
              endIndex=j;
              break;
            }
            else if(expression.charAt(j)=='a')
              noOfa++;

          }
          String firstPart=expression.substring(0,endIndex);
          String secondPart="";
          if(endIndex!=expression.length())
            secondPart=expression.substring(endIndex+1,expression.length());
          int j = 1;
          if((secondPart.equals(""))) {
            // This is an optimisation: If no second part, there cannot be any words assigned to the second part, so the first part must match everything.
            // It is equivalent to running the loop, because each time the second part will fail, because it is trying to match "" to a nonzero number of words.
            // This happens every time we have "1a2a3" with nothing after it, so it is tested by the unit tests already.
            j = words.length;
          }
          for(;j<=words.length;j++)
          {
            if(logDEBUG) Logger.debug(this, "2Making recursiveDoubleBarVerifier to consume "+j+" words");
            ParsedWord[] partToPassToDB = Arrays.copyOf(words, j);
            if(logDEBUG) Logger.debug(this, "3Calling recursiveDoubleBarVerifier with "+firstPart+" "+CSSPropertyVerifier.toString(partToPassToDB));
            if(recursiveDoubleBarVerifier(firstPart,partToPassToDB,cb)) //This function is written to verify || operator.
            {
              ParsedWord[] partToPass = Arrays.copyOfRange(words, j, words.length);
              if(logDEBUG) Logger.debug(this, "4recursiveDoubleBarVerifier true calling itself with "+secondPart+CSSPropertyVerifier.toString(partToPass));
              if(recursiveParserExpressionVerifier(secondPart,partToPass,cb))
                return true;
            }
            if(logDEBUG) Logger.debug(this, "5Back to recursiveDoubleBarVerifier "+j+" "+(noOfa+1)+" "+words.length);
          }
          return false;
        }
        else if(expression.charAt(i)==' ')
        {
          String firstPart=expression.substring(0,i);
          String secondPart=expression.substring(i+1,expression.length());
          if(words!=null && words.length>0)
          {
            int index=Integer.parseInt(firstPart);
            boolean result=CSSTokenizerFilter.auxilaryVerifiers[index].checkValidity(words[0], cb);
            if(result)
            {
              ParsedWord[] partToPass = Arrays.copyOfRange(words, 1, words.length);
              if(logDEBUG) Logger.debug(this, "8First part is true. partToPass="+CSSPropertyVerifier.toString(partToPass));
              if(recursiveParserExpressionVerifier(secondPart,partToPass, cb))
                return true;
            }
          }
          return false;
        }
        else if(expression.charAt(i)=='?')
        {
          String firstPart=expression.substring(0,i);
          String secondPart=expression.substring(i+1,expression.length());
          int index=Integer.parseInt(firstPart);
          if(words.length>0)
          {
            boolean result= CSSTokenizerFilter.auxilaryVerifiers[index].checkValidity(words[0], cb);
            if(result)
            {
              ParsedWord[] partToPass = Arrays.copyOfRange(words, 1, words.length);
              if(recursiveParserExpressionVerifier(secondPart,partToPass, cb))
                return true;
            }
          }
          else if(recursiveParserExpressionVerifier(secondPart,words, cb))
            return true;

          return false;
        }
        else if(expression.charAt(i)=='<')
        {
          int tindex=expression.indexOf('>');
          if(tindex>i)
          {
            int firstIndex=tindex+1;
            if((tindex!=expression.length()-1) && expression.charAt(tindex+1)=='[')
            {
              int indexOfSecondBracket=expression.indexOf(']');
              if(indexOfSecondBracket>(tindex+1))
              {
                tokensCanBeGivenLowerLimit=Integer.parseInt(expression.substring(tindex+2,indexOfSecondBracket).split(",")[0]);
                tokensCanBeGivenUpperLimit=Integer.parseInt(expression.substring(tindex+2,indexOfSecondBracket).split(",")[1]);
                firstIndex=expression.indexOf(']')+1;
              }
            }
            String firstPart=expression.substring(0,i);
            String secondPart=expression.substring(firstIndex,expression.length());
            if(secondPart.length() > 0 && secondPart.charAt(0) == ' ') {
              secondPart = secondPart.substring(1);
            } else if(secondPart.length() > 0) {
              throw new IllegalStateException("Don't know what to do with char after <>[]: "+secondPart.charAt(0));
            }
            if(logDEBUG) Logger.debug(this, "9in < firstPart="+firstPart+" secondPart="+secondPart+" tokensCanBeGivenLowerLimit="+tokensCanBeGivenLowerLimit+" tokensCanBeGivenUpperLimit="+tokensCanBeGivenUpperLimit);
            int index=Integer.parseInt(firstPart);
            String[] strLimits=expression.substring(i+1,tindex).split(",");
            if(strLimits.length==2)
            {
              int lowerLimit=Integer.parseInt(strLimits[0]);
              int upperLimit=Integer.parseInt(strLimits[1]);

              if(recursiveVariableOccuranceVerifier(index, words, lowerLimit,upperLimit,tokensCanBeGivenLowerLimit,tokensCanBeGivenUpperLimit, secondPart, cb))
                return true;
            }
          }

          return false;
        }

      }
      //Single verifier object
      if(logDEBUG) Logger.debug(this, "10Single token:"+expression);
      int index=Integer.parseInt(expression);
      return CSSTokenizerFilter.auxilaryVerifiers[index].checkValidity(words, cb);


    }
    /*
     * This function takes an array of string and concatenates everything in a " " seperated string.
     */
    public static String getStringFromArray(String[] parts,int lowerIndex,int upperIndex)
    {
      StringBuilder buffer=new StringBuilder();
      if(parts!=null && lowerIndex<parts.length)
      {
        for(int i=lowerIndex;i<upperIndex && i<parts.length;i++) {
          buffer.append(parts[i]);
                                        buffer.append(' ');
                                }
        return buffer.toString();
      }
      else
        return "";

    }
    /*
     * This function takes an array of string and concatenates everything in a " " seperated string.
     */
    public static String getStringFromArray(String[] parts) {
      return getStringFromArray(parts, 0, parts.length-1);
    }
    //Creates a new sub array from the main array and returns it.
    public static ParsedWord[] getSubArray(ParsedWord[] array,int lowerIndex,int upperIndex)
    {
      ParsedWord[] arrayToReturn=new ParsedWord[upperIndex-lowerIndex];
      if(array!=null && lowerIndex<array.length)
      {
        for(int i=lowerIndex;i<upperIndex && i<array.length;i++)
        {
          arrayToReturn[i-lowerIndex]=array[i];
        }
        return arrayToReturn;
      }
      else
        return new ParsedWord[0];
    }
    /*
     * For verifying part of the ParseExpression with [] operator.
     */
    public boolean recursiveVariableOccuranceVerifier(int verifierIndex,ParsedWord[] valueParts,int lowerLimit,int upperLimit,int tokensCanBeGivenLowerLimit,int tokensCanBeGivenUpperLimit, String secondPart, FilterCallback cb)
    {

      if(logDEBUG) Logger.debug(this, "recursiveVariableOccurranceVerifier("+verifierIndex+","+toString(valueParts)+","+lowerLimit+","+upperLimit+","+tokensCanBeGivenLowerLimit+","+tokensCanBeGivenUpperLimit+","+secondPart+")");
      if((valueParts==null || valueParts.length==0) && lowerLimit == 0)
        return true;

      if(lowerLimit <= 0) {
        // There could be secondPart.
        if(recursiveParserExpressionVerifier(secondPart, valueParts, cb)) {
          if(logDEBUG) Logger.debug(this, "recursiveVariableOccurranceVerifier completed by "+secondPart);
          return true;
        }
      }

      // There can be no more parts.
      if(upperLimit == 0) {
        if(logDEBUG) Logger.debug(this, "recursiveVariableOccurranceVerifier: no more parts");
        return false;
      }

      for(int i=tokensCanBeGivenLowerLimit; i<=tokensCanBeGivenUpperLimit && i <= valueParts.length;i++) {
        ParsedWord[] before = Arrays.copyOf(valueParts, i);
        if(CSSTokenizerFilter.auxilaryVerifiers[verifierIndex].checkValidity(before, cb)) {
          if(logDEBUG) Logger.debug(this, "first "+i+" tokens using "+verifierIndex+" match "+toString(before));
          if(i == valueParts.length && lowerLimit <= 1) {
            if(recursiveParserExpressionVerifier(secondPart, new ParsedWord[0], cb)) {
              if(logDEBUG) Logger.debug(this, "recursiveVariableOccurranceVerifier completed with no more parts by "+secondPart);
              return true;
            } else {
              if(logDEBUG) Logger.debug(this, "recursiveVariableOccurranceVerifier: satisfied self but nothing left to match "+secondPart);
              return false;
            }
          } else if(i == valueParts.length && lowerLimit > 1)
            return false;
          ParsedWord[] after = Arrays.copyOfRange(valueParts, i, valueParts.length);
          if(logDEBUG) Logger.debug(this, "rest of tokens: "+toString(after));
          if(recursiveVariableOccuranceVerifier(verifierIndex, after, lowerLimit-1, upperLimit-1, tokensCanBeGivenLowerLimit, tokensCanBeGivenUpperLimit, secondPart, cb))
            return true;
        }
      }

      return false;
    }

    static String toString(ParsedWord[] words) {
      if(words == null) return null;
      StringBuilder sb = new StringBuilder();
      boolean first = true;
      for(ParsedWord word : words) {
        if(!first) sb.append(",");
        first = false;
        sb.append(word);
      }
      return sb.toString();
    }

    /*
     * This function verifies || operator. This function returns true only if it can consume entire value for the given parseExpression
     * e.g. expression is [1 || 2 || 3 || 4] and value is "Hello world program"
     * Then this function would try all combinations of objects so that the expression can consume this value.
     * 1 would try to consume "Hello" and rest would try to consume "world program"
     * 2 would try to consume "Hello" and rest would try to consume "world program"
     * 3 would try to consume "Hello" and rest would try to consume "world program"
     * and so on.
     */
    public boolean recursiveDoubleBarVerifier(String expression,ParsedWord[] words,FilterCallback cb)
    {
      if(logDEBUG) Logger.debug(this, "11in recursiveDoubleBarVerifier expression="+expression+" value="+toString(words));
      if(words==null || words.length == 0)
        return true;

      String ignoredParts="";
      String firstPart = "";
      String secondPart = "";
      int lastA = -1;
      // Check for invalid patterns.
      assert(expression.length() != 0);
      assert(expression.charAt(expression.length()-1) != 'a');
      assert(expression.charAt(0) != 'a');
      for(int i=0;i<=expression.length();i++)
      {
        if(i == expression.length() || expression.charAt(i)=='a')
        {
          if(!firstPart.equals("")) {
            if(ignoredParts.length() == 0)
              ignoredParts = firstPart;
            else
              ignoredParts = ignoredParts+"a"+firstPart;
          }
          else ignoredParts = "";
          firstPart=expression.substring(lastA+1,i);
          lastA = i;
          if(i == expression.length())
            secondPart = "";
          else
            secondPart=expression.substring(i+1,expression.length());
          if(logDEBUG) Logger.debug(this, "12in a firstPart="+firstPart+" secondPart="+secondPart+" for expression "+expression+" i "+i);

          boolean result=false;

          int index=Integer.parseInt(firstPart);
          for(int j=0;j<words.length;j++)
          {
            // Check the first j+1 words against this verifier: A single verifier can consume more than one word.
            result=CSSTokenizerFilter.auxilaryVerifiers[index].checkValidity(getSubArray(words, 0, j+1), cb);
            if(logDEBUG) Logger.debug(this, "14in for loop result:"+result+" for "+toString(words)+" for "+firstPart);
            if(result)
            {
              // Check the remaining words...
              ParsedWord[] valueToPass = Arrays.copyOfRange(words, j+1, words.length);
              if(valueToPass.length == 0) {
                // We have matched everything against the subset we have considered so far.
                if(logDEBUG) Logger.debug(this, "14opt No more words to pass, have matched everything");
                return true;
              }
              // Against the rest of the pattern: the part that we've tried and failed plus the part that we haven't tried yet.
              // NOT against the verifier we were just considering, because the double-bar operator expects no more than one match from each component of the pattern.
              String pattern = ignoredParts+((("".equals(ignoredParts))||("".equals(secondPart)))?"":"a")+secondPart;
              if(logDEBUG) Logger.debug(this, "14a "+toString(getSubArray(words, 0, j+1))+" can be consumed by "+index+ " passing on expression="+pattern+ " value="+toString(valueToPass));
              if(pattern.equals("")) return false;
              result=recursiveDoubleBarVerifier(pattern,valueToPass, cb);
              if(result)
              {
                if(logDEBUG) Logger.debug(this, "15else part is true, value consumed="+words[j]);
                return true;
              }
            }
          }
        }
      }

      if(lastA != -1) return false;
      //Single token
      int index=Integer.parseInt(expression);
      if(logDEBUG) Logger.debug(this, "16Single token:"+expression+" with value=*"+Fields.commaList(words)+"* validity="+CSSTokenizerFilter.auxilaryVerifiers[index].checkValidity(words,cb));
      return CSSTokenizerFilter.auxilaryVerifiers[index].checkValidity(words,cb);


    }





  }
  //CSSPropertyVerifier class extended for verifying content property.
  static class ContentPropertyVerifier extends CSSPropertyVerifier
  {

    ContentPropertyVerifier(Collection<String> allowedValues)
    {
      super(allowedValues,null,null,null);
    }

    @Override
    public boolean checkValidity(String[] media,String[] elements,ParsedWord[] value,FilterCallback cb)
    {
      if(logDEBUG) Logger.debug(this, "ContentPropertyVerifier checkValidity called: "+toString(value));

      if(value.length != 1) return false;

      if(value[0] instanceof ParsedIdentifier && allowedValues!=null && allowedValues.contains(((ParsedIdentifier)value[0]).getDecoded()))
        return true;

      //String processing
      if(value[0] instanceof ParsedString) {
        if(ElementInfo.ALLOW_ALL_VALID_STRINGS || ElementInfo.isValidStringDecoded(((ParsedString)value[0]).getDecoded()))
          return true;
        else
          return false;
      }

      if(value[0] instanceof ParsedCounter) {
        ParsedCounter counter = (ParsedCounter)value[0];
        if(counter.listType != null) {
          HashSet<String> listStyleType=new HashSet<String>();
          listStyleType.add("disc");
          listStyleType.add("circle");
          listStyleType.add("square");
          listStyleType.add("decimal");
          listStyleType.add("decimal-leading-zero");
          listStyleType.add("lower-roman");
          listStyleType.add("upper-roman");
          listStyleType.add("lower-greek");
          listStyleType.add("lower-latin");
          listStyleType.add("upper-latin");
          listStyleType.add("armenian");
          listStyleType.add("georgian");
          listStyleType.add("lower-alpha");
          listStyleType.add("upper-alpha");
          listStyleType.add("none");
          if(!listStyleType.contains(counter.listType.getDecoded())) return false;
        }
        if(counter.separatorString != null && !(ElementInfo.ALLOW_ALL_VALID_STRINGS || ElementInfo.isValidStringDecoded(counter.separatorString.getDecoded())))
          return false;
        return true;
      }

      if(value[0] instanceof ParsedAttr) {
        return true;
      }

      if(value[0] instanceof ParsedURL) {
        // CONFORMANCE: This is required by the spec, and quite useful in practice.
        // Browsers in practice only support images here.
        // However, as long as they respect the MIME type - and if they don't we are screwed anyway - this should be safe even if it allows including text, CSS and HTML.
        // Note also that generated content cannot alter the parse tree, so what can be done is presumably severely limited.
        return isValidURI((ParsedURL)value[0], cb);
      }

      return false;

    }



  }

  //For verifying ’font-size’[ / ’line-height’]? of Font property

  static class FontPartPropertyVerifier extends CSSPropertyVerifier
  {
    FontPartPropertyVerifier() {
      super(false);
    }

    @Override
    public boolean checkValidity(String[] media,String[] elements,ParsedWord[] value,FilterCallback cb)
    {

      if(logDEBUG) Logger.debug(this, "FontPartPropertyVerifier called with "+toString(value));
      CSSPropertyVerifier fontSize=new CSSPropertyVerifier(Arrays.asList("xx-small","x-small","small","medium","large","x-large","xx-large","larger","smaller","inherit"),Arrays.asList("le","pe"),null,null,true);
      if(fontSize.checkValidity(value, cb)) return true;

      for(ParsedWord word : value) {
        // Token by token
        if(fontSize.checkValidity(word, cb)) continue;
        if(word instanceof SimpleParsedWord) {
          String orig = ((SimpleParsedWord)word).original;
          if(orig.indexOf("/")!=-1)
          {
            int slashIndex=orig.indexOf("/");
            String firstPart=orig.substring(0,slashIndex);
            String secondPart=orig.substring(slashIndex+1,orig.length());
            if(logDEBUG) Logger.debug(this, "FontPartPropertyVerifier FirstPart="+firstPart+" secondPart="+secondPart);
            CSSPropertyVerifier lineHeight=new CSSPropertyVerifier(Arrays.asList("normal","inherit"),Arrays.asList("le","pe","re","in"),null,null,true);
            ParsedWord[] first = split(firstPart,false);
            ParsedWord[] second = split(secondPart,false);
            if(first.length == 1 && second.length == 1 &&
                fontSize.checkValidity(first, cb) && lineHeight.checkValidity(second,cb))
              continue;
          }
        }
        return false;

      }
      return true;

    }

  }

  static abstract class FamilyPropertyVerifier extends CSSPropertyVerifier {

    FamilyPropertyVerifier(boolean valueOnly, Set<String> mediaTypes) {
      super(null, mediaTypes, null, null, valueOnly, true);
    }

    // FIXME: We do not change the tokens.
    // We probably should put in quotes around unquoted font family names, put spaces after commas etc, but
    // this may be a bit tricky: we'd have to put spaces etc inside some words, and delete some words...
    // Quite possible, but not a high priority, "verdana,arial,times new roman,sans-serif" is not dangerous, it's just hard to parse.

    @Override
    public boolean checkValidity(String[] media,String[] elements,ParsedWord[] value,FilterCallback cb)
    {
      if(logDEBUG) Logger.debug(this, "font verifier: "+toString(value));
      if(value.length == 1) {
        if(value[0] instanceof ParsedIdentifier && "inherit".equalsIgnoreCase(((ParsedIdentifier)value[0]).original)) {
        //CSS Property has one of the explicitly defined values
          if(logDEBUG) Logger.debug(this, "font: inherit");
          return true;
        }
      }
      if(allowedMedia!=null && !onlyValueVerifier) {
        boolean allowed = false;
        for(String m : media)
          if(allowedMedia.contains(m)) {
            allowed = true;
            break;
          }
        if(!allowed) {
          if(logDEBUG) Logger.debug(this, "checkValidity Media of the element is not allowed.Media="+Fields.commaList(media)+" allowed Media="+allowedMedia.toString());

          return false;
        }
      }
      ArrayList<String> fontWords = new ArrayList<String>();
      // FIXME delete fonts we don't know about but let through ones we do.
      // Or allow unknown fonts given [a-z][A-Z][0-9] ???
outer:    for(int i=0;i<value.length;i++) {
        ParsedWord word = value[i];
        String s = null;
        if(word instanceof ParsedString) {
          String decoded = (((ParsedString)word).getDecoded());
          if(logDEBUG) Logger.debug(this, "decoded: \""+decoded+"\"");
          // It's actually quoted, great.
          if(isSpecificFamily(decoded.toLowerCase())) {
            continue;
          } if(isGenericFamily(decoded.toLowerCase())) {
            continue;
          } else
            s = decoded;
        } else if(word instanceof ParsedIdentifier) {
          s = (((ParsedIdentifier)word).getDecoded());
          if(isGenericFamily(s)) {
            continue;
          }
          if(isSpecificFamily(s)) {
            continue;
          }
          if(word.postComma) {
            if(logDEBUG) Logger.debug(this, "Word ends in comma, but is not a valid font on its own: "+word+" (index "+i+")");
            return false;
          }
        } else
          return false;
        // Unquoted multi-word font, or unquoted single-word font.
        // Unfortunately fonts can be ambiguous...
        // Therefore we do not accept a single-word font unless it is either quoted or ends in a comma.
        fontWords.clear();
        assert(s != null);
        fontWords.add(s);
        if(logDEBUG) Logger.debug(this, "first word: \""+s+"\"");
        if(i == value.length-1) {
          if(logDEBUG) Logger.debug(this, "last word. font words: "+getStringFromArray(fontWords.toArray(new String[fontWords.size()]))+" valid="+validFontWords(fontWords));
          return validFontWords(fontWords);
        }
        if(!possiblyValidFontWords(fontWords))
          return false;
        boolean last = false;
        for(int j=i+1;j<value.length;j++) {
          ParsedWord newWord = value[j];
          if (j == value.length-1) last = true;
          String s1;
          if(newWord instanceof ParsedIdentifier) {
            s1 = ((ParsedIdentifier)newWord).original;
            fontWords.add(s1);
            if(logDEBUG) Logger.debug(this, "adding word: \""+s1+"\"");
            if(last) {
              if(newWord.postComma) {
                if(logDEBUG) Logger.debug(this, "not valid: trailing comma at end");
              }
              if(validFontWords(fontWords)) {
                // Valid. Good.
                if(logDEBUG) Logger.debug(this, "font: reached last in inner loop, valid. font words: "+getStringFromArray(fontWords.toArray(new String[fontWords.size()])));
                return true;
              }
            }
            if(newWord.postComma) {
              // Words must be a valid font when put together
              if(validFontWords(fontWords)) {
                fontWords.clear();
                i = j;
                continue outer;
              } else {
                if(logDEBUG) Logger.debug(this, "comma but can't parse font words: "+Fields.commaList(fontWords.toArray(new String[fontWords.size()])));
                return false;
              }
            }
          } else {
            if(logDEBUG) Logger.debug(this, "cannot parse "+newWord);
            return false;
          }
        }
        // Still looking for another keyword...
        if(validFontWords(fontWords))
          return true;
        return false;
      }
      if(logDEBUG) Logger.debug(this, "font: reached end, valid");
      return true;
      }

    private boolean possiblyValidFontWords(ArrayList<String> fontWords) {
      if(ElementInfo.disallowUnknownSpecificFonts) {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for(String s : fontWords) {
          if(!first) sb.append(' ');
          first = false;
          sb.append(s);
        }
        String s = sb.toString().toLowerCase();
        return ElementInfo.isWordPrefixOrMatchOfSpecificFontFamily(s);
      } else {
        for(String s : fontWords)
          if(!isSpecificFamily(s)) return false;
        return true;
      }
    }

    private boolean validFontWords(ArrayList<String> fontWords) {
      for(String s : fontWords) {
        if(s == null) throw new NullPointerException();
      }
      if(fontWords.size() == 1) {
        if(isGenericFamily(fontWords.get(0).toLowerCase()))
          return true;
      }
      StringBuilder sb = new StringBuilder();
      boolean first = true;
      for(String s : fontWords) {
        if(!first) sb.append(' ');
        first = false;
        sb.append(s);
      }
      return isSpecificFamily(sb.toString().toLowerCase());
    }

    abstract boolean isSpecificFamily(String s);

    abstract boolean isGenericFamily(String s);

  }

  static class FontPropertyVerifier extends FamilyPropertyVerifier {

    FontPropertyVerifier(boolean valueOnly) {
      super(valueOnly, ElementInfo.VISUALMEDIA);
    }

    @Override
    boolean isSpecificFamily(String s) {
      return ElementInfo.isSpecificFontFamily(s);
    }

    @Override
    boolean isGenericFamily(String s) {
      return ElementInfo.isGenericFontFamily(s);
    }

  }

  static class VoiceFamilyPropertyVerifier extends FamilyPropertyVerifier {

    VoiceFamilyPropertyVerifier(boolean valueOnly) {
      super(valueOnly, ElementInfo.AURALMEDIA);
    }

    @Override
    boolean isSpecificFamily(String s) {
      return ElementInfo.isSpecificVoiceFamily(s);
    }

    @Override
    boolean isGenericFamily(String s) {
      return ElementInfo.isGenericVoiceFamily(s);
    }

  }

  public static String removeOuterQuotes(String decoded) {
    if(decoded.length() < 2) return decoded;
    char first = decoded.charAt(0);
    if(!(first == '\'' || first == '\"')) return decoded;
    if(decoded.charAt(decoded.length()-1) == first) {
      return decoded.substring(1, decoded.length()-1);
    }
    return decoded;
  }

  public String detectedCharset() {
    return detectedCharset;
  }

    public static void main(String arg[]) throws Throwable {
        final File fin = new File("/tmp/test.css");
        final File fout = new File("/tmp/test2.css");
        fout.delete();
        final Bucket inputBucket = new FileBucket(fin, true, false, false, false);
        final Bucket outputBucket = new FileBucket(fout, false, true, false, false);
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = inputBucket.getInputStream();
            outputStream = outputBucket.getOutputStream();
            Logger.setupStdoutLogging(Logger.LogLevel.DEBUG, "");

            ContentFilter.filter(inputStream, outputStream, "text/css",
                    new URI("http://127.0.0.1:8888/freenet:USK@ZupQjDFZSc3I4orBpl1iTEAPZKo2733RxCUbZ2Q7iH0,EO8Tuf8SP3lnDjQdAPdCM2ve2RaUEN8m-hod3tQ5oQE,AQACAAE/jFreesite/19/Style/"), null, null, null);
        } finally {
            Closer.close(inputStream);
            Closer.close(outputStream);
            inputBucket.free();
            outputBucket.free();
        }
    }
}
TOP

Related Classes of freenet.client.filter.CSSTokenizerFilter$ParsedCounter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.