All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.myfaces.trinidadinternal.style.util.CSSGenerationUtils Maven / Gradle / Ivy

The newest version!
/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.myfaces.trinidadinternal.style.util;

import java.beans.Beans;

import java.io.PrintWriter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.regex.Pattern;

import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidadinternal.agent.TrinidadAgent;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.SkinSelectors;
import org.apache.myfaces.trinidadinternal.style.StyleContext;
import org.apache.myfaces.trinidadinternal.style.xml.parse.PropertyNode;
import org.apache.myfaces.trinidadinternal.style.xml.parse.StyleNode;


/**
 * CSS-generation related utilities used when we write out our css-2 stylesheet
 * document based on the skin's css-3 stylesheet document.
 *
 * @version $Name:  $ ($Revision: adfrt/faces/adf-faces-impl/src/main/java/oracle/adfinternal/view/faces/style/util/CSSUtils.java#0 $) $Date: 10-nov-2005.18:58:49 $
 */

public class CSSGenerationUtils
{
  /**
   * Converts the specified set of StyleNodes to CSS. We output either full styleclass names or
   * compressed styleclass names.
   *
   * @param context The current StyleContext
   * @param styleSheetName The stylesheet name that is registered with the skin.
   *     e.g. skins/purple/purpleSkin.css
   * @param styles The style nodes to convert
   * @param writerFactory The factory to obtain {@link PrintWriter} instances
   * @param compressStyles This tells us whether or not we want to output the short names.
   * @param shortStyleClassMap A Map which maps style
   *   class names to short names.
   * @param namespacePrefixArray an array with the namespace prefixes of our
   *  special selectors. e.g., "af|" or "tr|" .
   * @param afSelectorMap A Map which maps the namespaced component selectors
   *   to their base names (e.g., 'af|menuPath::step' maps to 'af|menuPath A')
   */
  public static void writeCSS(
    StyleContext        context,
    String              styleSheetName,
    StyleNode[]         styles,
    StyleWriterFactory  writerFactory,
    boolean             compressStyles,
    Map shortStyleClassMap,
    String[]            namespacePrefixArray,
    Map afSelectorMap
    )
  {
    PrintWriter out = writerFactory.createWriter();
    if (out == null)
    {
      return;
    }

    // writeCSS() attempts to produce a minimal set of style rules
    // by combining selectors with identical properties into a
    // single rule.  For example, we convert the following two
    // styles:
    //
    //   .OraBGAccentVeryDark,.x8 {background-color:#999966}
    //   .OraTable,.x1x {background-color:#999966}
    //
    // into a single style:
    //
    //   .OraBGAccentVeryDark,.x8,.OraTable,.x1x {background-color:#999966}
    //
    // The intention of this optimization is to improve performance by
    // reducing the overall size of the generated CSS file - and hopefully
    // speed up download time.
    //
    // To implement this optimization, we perform two passes over the
    // set of styles.  During the first pass, we build up a hash map
    // which maps from the property string to an array of StyleNodes
    // which share the properties defined by the property string.
    // During the second pass over the styles, we write out all matching
    // selectors for each style followed by the shared set of properties.

    // We track styles with matching properties in the following HashMap
    // which maps property strings to StyleNode[]s.
    HashMap matchingStylesMap =
      new HashMap(101);

    // We also keep an array of the property strings that we generate
    // during this pass, since we need these strings during the second
    // pass to find matching StyleNodes.
    String[] propertyStrings = new String[styles.length];

    // at this point the styles StyleNode[] can contain both Styles with
    // non-null selector or non-null name(aka alias). We only generate
    // the styles where getSelector is non-null.
    for (int i = 0; i < styles.length; i++)
    {
      StyleNode style = styles[i];

      if (style.getSelector() != null)
      {
        // Get the property string (properties are sorted so that
        // the order doesn't affect whether styles match).
        String propertyString = _getSortedPropertyString(style);

        // See if we already have a StyleNode with the same properties
        StyleNode[] matchingStyles = matchingStylesMap.get(propertyString);

        if (matchingStyles == null)
        {
          // If we don't already have matching StyleNodes, add this
          // StyleNode to the map.
          propertyStrings[i] = propertyString;
          matchingStyles = new StyleNode[1];
          matchingStyles[0] = style;
        }
        else
        {
          // If we already have matching StyleNodes, add this StyleNode
          // to the end of the list of matching StyleNodes.
          int length = matchingStyles.length;

          StyleNode[] newMatchingStyles = new StyleNode[length + 1];
          System.arraycopy(matchingStyles,
                           0,
                           newMatchingStyles,
                           0,
                           length);
          newMatchingStyles[length] = style;
          matchingStyles = newMatchingStyles;
        }

        // Rehash with the new value
        matchingStylesMap.put(propertyString, matchingStyles);
      }
    }

    // We'll start writing the CSS file now.  First
    // write out the header with a time stamp
    Date date = new Date();
    if (!compressStyles)
      out.println("/* This CSS file generated on " + date + " */");

    // Keep track of the number of selectors written out. The reason? IE has a 4095 limit,
    // and we want to warn when we get to that limit.
    int numberSelectorsWritten = 0;

    // This is the second pass in which we write out the style rules
    // Get the baseURI up front so we don't have to recalculate it every time we find
    // a property value that contains url() and need to resolve the uri.
    String baseURI = CSSUtils.getBaseSkinStyleSheetURI(styleSheetName);
    for (int i = 0; i < styles.length; i++)
    {
      StyleNode style = styles[i];
      String propertyString = propertyStrings[i];

      // We only write out styles for which we have a property string.
      // All other entries correspond to styles which don't have selectors -
      // or styles which will be rendered as a "matching" style.
      if (propertyString != null && !(propertyString.equals("")))
      {
        // Get all of the styles which share this property string.
        StyleNode[] matchingStyles = matchingStylesMap.get(propertyString);

        // Actually, we should always have at least one StyleNode here
        assert (matchingStyles != null);

        // determine if the current CSS file can fit all of the CSS selectors, or if a new
        // one will be needed.
        // TODO: figure out why we write both the uncompressed & compressed styles for styles
        // without a '|' character, shouldn't the uncompressed be enough on its own? This results
        // in some ugly code here.
        int stylesToBeWritten = 0;
        String[] selectors = new String[matchingStyles.length];
        String[] mappedSelectors = new String[matchingStyles.length];

        for (int j = 0; j < matchingStyles.length; j++)
        {
          selectors[j] = matchingStyles[j].getSelector();

          // We should always have a selector at this point
          assert (selectors[j] != null);

          mappedSelectors[j] = getMappedSelector(afSelectorMap,
                                                 namespacePrefixArray,
                                                 selectors[j]);

          if (compressStyles && (mappedSelectors[j].indexOf('|') == -1))
          {
            stylesToBeWritten += 2;
          }
          else
          {
            stylesToBeWritten++;
          }
        }

        if (numberSelectorsWritten + matchingStyles.length >= _MSIE_SELECTOR_LIMIT
          && TrinidadAgent.Application.IEXPLORER == context.getAgent().getAgentApplication())
        {
          out.println("/* The number of CSS selectors in this file is " +
                      numberSelectorsWritten + " */");
          out = writerFactory.createWriter();
          if (out == null)
          {
            return;
          }
          numberSelectorsWritten = 0;
        }

        // Write out all of the style selectors for this property string
        for (int j = 0; j < matchingStyles.length; j++)
        {
          String validFullNameSelector = null;

          // write out the full selector if we aren't compressing styles or
          // it doesn't have a '|' in the name which means it may be a user's public styleclass
          // and we don't want to compress those; we will also write out the compressed
          // version the public styleclasses in the next step.
          if (!compressStyles || (mappedSelectors[j].indexOf('|') == -1))
          {
            validFullNameSelector =
              getValidFullNameSelector(mappedSelectors[j], namespacePrefixArray);

            if (validFullNameSelector != null)
            {
              out.print(validFullNameSelector);
              numberSelectorsWritten++;
            }
          }



          if (compressStyles)
          {
            String shortSelector = 
              getShortSelector(shortStyleClassMap, namespacePrefixArray, mappedSelectors[j]);

            // if the transformed full name is different than the shortSelector
            // then write out the shortSelector, too.
            if (shortSelector != null)
            {
              String validShortSelector =
                getValidFullNameSelector(shortSelector, namespacePrefixArray);

              // if we wrote out a full style, check to see if we need to write out the short, too.
              // if it is something different, write out the short, too.
              if (validFullNameSelector != null)
              {
                //Since validFullNameSelector is not null, we know we wrote out a full style
                // we write out a short style too in this case if it is different
                // example: .PublicStyleClass is written out fully even in compressed mode, but
                // it is different in compressed mode, so we write that out, too.
                if (!validFullNameSelector.equals(validShortSelector))
                {
                  out.print(',');
                  out.print(validShortSelector);
                  numberSelectorsWritten++;
                }
              }
              else
              {
                out.print(validShortSelector);
                numberSelectorsWritten++;
              }
            }
          }

          // Write out a separator between matching selectors
          if (j < (matchingStyles.length - 1))
            out.print(",");
        }

        // Now that we have written out the selectors, write out
        // the properties
        out.print(" {");

        // At this point, we could just write out the property string
        // that we already created, but this string contains the properties
        // in sorted order.  We prefer to attempt to preserve the order
        // of the properties as specified in the XSS document.  So,
        // we get the properties from the StyleNode object instead of
        // using the propertyString
        Iterable properties = style.getProperties();
        boolean first = true;

        for (PropertyNode property : properties)
        {
          String propName = property.getName();
          String propValue = property.getValue();

          if ((propName != null) &&
              (propValue != null) &&
              (propValue.length() > 0 ))
          {
            if (!first)
              out.print(";");
            else
              first = false;

            out.print(propName);
            out.print(":");
            String resolvedPropValue =
              CSSUtils.resolvePropertyValue(styleSheetName, baseURI, propName, propValue);
            out.print(resolvedPropValue);

          }
        }

        if (compressStyles)
          out.print("}"); // take out the newlines for performance
        else
          out.println("}");
      }
    }
    out.println("/* The number of CSS selectors in this file is " + numberSelectorsWritten + " */");
  }

  /**
   * Shorten (compress) the selector.
   * @param shortStyleClassMap
   * @param namespacePrefixArray
   * @param selector
   * @return the shortened selector, or selector if nothing could be shortened.
   */
  public static String getShortSelector(
    Map shortStyleClassMap,
    String[]            namespacePrefixArray,
    String              selector)
  {
    // shorten all the css-2 style class selectors (those that start with
    // '.' and don't have a namespace prefix in it)
    // and return the shortened string.
    // e.g., selector of '.OraBulletedList A' is shortened to '.xj A'
    // e.g., selector of af|inputText::content is not shortened since
    // it has no css-2 style class selector piece that starts with '.'.
    // e.g., selector of af|foo.Bar will shorten the '.Bar' piece
    // af|foo.xz
    // e.g., .Foo:hover -> .x0:hover
    String shortSelector = _getShortNonNamespacedSelector(selector,
                                             shortStyleClassMap);

    if (shortSelector == null)
      shortSelector = selector;

    // run it through a shortener one more time to shorten any
    // of the af component selectors.
    // e.g., 'af|menuPath' is shortened to '.x11'

    if (_hasNamespacePrefix(shortSelector, namespacePrefixArray))
    {
      String[] shortSelectorArray  = _splitStringByWhitespace(shortSelector);

      shortSelector =
        _getMappedNSSelector(shortStyleClassMap,
                             namespacePrefixArray,
                             shortSelector,
                             shortSelectorArray,
                             true);
    }
    return shortSelector;
  }

  /**
   * Tests whether the specified selector is a single style class
   * selector. A single style class selector is something like
   * ".AFInstructionText". Examples that are not single style class
   * selectors are "af|inputText" or ".AFFoo .AFBar" or ".foo:hover"
   */
  public static boolean isSingleStyleClassSelector(String selector)
  {
    if ((selector == null)      ||
        (selector.length() < 2) ||
        (selector.charAt(0) != '.'))
    {
      return false;
    }

    for (int i = 1; i < selector.length(); i++)
    {
      if (_isStyleClassTerminator(selector.charAt(i)))
        return false;
    }

    return true;
  }

  /**
   * Returns an
   * Iterator of all of the style class selectors included in the
   * specified selector.  For example, ".OraNav1Enabled" returns
   * a single element Iterator with the string "OraNav1Enabled".
   * "P.OraNav1Enabled SPAN.text" returns a two element Iterator
   * with "OraNav1Enabled" and "text".
   * .OraLink:visited returns "OraLink"
   * .star.moon returns "star" and "moon"
   */
  public static Iterator getStyleClasses(String selector)
  {
    ArrayList styleClasses = null;
    int styleClassStartIndex = -1;
    int length = selector.length();

    for (int i = 0; i < length; i++)
    {
      char c = selector.charAt(i);

      // If we aren't inside of a style class yet,
      // check to see if we are about to enter one.
      // Otherwise, check to see if we are at the
      // end of one.
      if (styleClassStartIndex == -1)
      {
        if (c == '.')
          styleClassStartIndex = i;
      }
      else
      {
        boolean end = _isStyleClassTerminator(c);
        if (!end)
        {
          // Check to see if we are at the end of the string
          if (i == (length - 1))
          {
            // We need to increment our counter to make sure
            // we include this last character when we pull out
            // the substring
            i++;
            end = true;
          }
        }

        // If we're at the end of the style class, add it to the list
        if (end)
        {
          String styleClass = selector.substring(styleClassStartIndex + 1, i);
          if (styleClasses == null)
            styleClasses = new ArrayList(3);

          styleClasses.add(styleClass);
          // if c is ., then this means we've found another styleclass
          if (c == '.')
            styleClassStartIndex = i;
          else
            styleClassStartIndex = -1;
        }
      }
    }

    if (styleClasses == null)
      return null;

    return styleClasses.iterator();
  }

  /**
   * Called when creating the shortened styleclass map.
   * Returns an Iterator of all of the component selectors that begin with the
   * given namespace. All styleclasses and pseudo-classes are ignored and NOT
   * returned.
   * For example, ".OraNav1Enabled af|menuPath" returns
   * a single element Iterator with the string "af|menuPath".
   * "af|menuPath.OraNav1Enabled af|treeTable.text" returns a two
   * element Iterator with "af|menuPath" and "af|treeTable".
   * af|inputText:disabled af|inputText::content returns two
   * "af|inputText" and "af|inputText::content".
   * It also looks at the afSelectorMap to map any special selectors if
   * necessary.
   * e.g., "af|menuPath::step" maps to "af|menuPath A", so
   * ".OraNav1Enabled af|menuPath::step" returns "af|menuPath"
   */
  public static Iterator getNamespacedSelectors(
    String              selector,
    String              namespace,
    Map afSelectorMap)
  {
    if (selector == null)
      return null;
    int afIndex = selector.indexOf(namespace);

    // no namespace in the selector, so just return null
    if (afIndex == -1)
      return null;

    ArrayList afUnmappedSelectorList = new ArrayList();

    // now find each af| component selector and map
    // e.g., af|menuPath::step maps to af|menuPath A

    // order the pseudo elements and classes
    String base = selector.substring(afIndex);
    String[] afSelectors =
        _orderPseudoElementsAndClasses(base);

    // loop through each of the af| parts.
    for (int i=0; i < afSelectors.length; i++)
    {
      if (afSelectors[i].startsWith(namespace))
      {
        // get the component selector, which is just the main part, nothing
        // that includes a '.' ':', or a ' '.
        String afComponentSelector =
          _getNSComponentSelector(afSelectors[i], false);
        afUnmappedSelectorList.add(afComponentSelector);
      }
    }

    // ok, now everything in afSelectorList at this point is the unmapped
    // af|xxx component selectors. Now map them, and return the base key
    // in the selector.
    // loop through again and map them
    ArrayList afSelectorList = new ArrayList();

    int size = afUnmappedSelectorList.size();
    for (int i=0; i < size; i++)
    {
      String afComponentSelector = afUnmappedSelectorList.get(i);
      String mappedSelector = null;

      if (afSelectorMap != null)
      {
        mappedSelector = afSelectorMap.get(afComponentSelector);

      }
      if (mappedSelector != null)
      {
        // In case the mapped selector does not start with the namespace,
        // start the string where the namespace starts.
        int namespaceIndex = mappedSelector.indexOf(namespace);
        if (namespaceIndex > -1)
        {
          // get the selector up until the space.
          String[] baseSelector = _splitStringByWhitespace(mappedSelector.substring(namespaceIndex));
          afComponentSelector = baseSelector[0];
          afSelectorList.add(afComponentSelector);
        }
      }
      else
      {
        afSelectorList.add(afComponentSelector);
      }

    }
    return afSelectorList.iterator();

  }

  /**
   * Add to the namespacePrefixes Set any namespace prefixes found in this selector.
   * @param namespacePrefixes
   * @param selector
   */
  public static void getNamespacePrefixes(
    Set namespacePrefixes,
    String selector)
  {

    int length = selector.length();
    int startSubstringIndex = 0;
    // Loop through each character of the selector looking for namespace prefixes.
    for (int i = 0; i < length; i++)
    {
      char c = selector.charAt(i);
      if (c == '|')
      {
        String prefix = selector.substring(startSubstringIndex, i+1);
        startSubstringIndex = i+1;
        // protect against just | in the prefix by checking length.
        if (prefix.length() > 1)
          namespacePrefixes.add(prefix);
      }
      else if(!_isStyleClassTerminator(c))
      {
        // keep going if it isn't a terminating character
      }
      else
      {
        // update the startSubstring index.
        startSubstringIndex = i+1;
      }
    }
    return;
  }

  /**
   * Called from getNamespacedSelectors.
   * Given a single selector that begins with a namespace prefix (most likely
   * af|), return the main portion -- the prefix+component+pseudo-element.
   * Styleclasses and pseudo-classes and anything after a space will be ignored,
   * and won't be returned.
   * @param singleAfSelector
   * @param allowPseudoClass
   * @return
   */
  private static String _getNSComponentSelector(
    String singleAfSelector,
    boolean allowPseudoClass)
  {
    int colonIndex = singleAfSelector.indexOf("::");

    // get the part after the ::.
    // colonIndex will be 0 if there is no ::.
    if (colonIndex != -1)
    {
      colonIndex += 2;
    }
    else
      colonIndex = 0;
    String afterDoubleColon = singleAfSelector.substring(colonIndex);

    // now find the part with a single ':', a ' ', or a '.'. That is where
    // I want to chop it to get my component selector.

    boolean end = false;
    int afterLength = afterDoubleColon.length();
    int endIndex=0;

    for (; ((endIndex < afterLength) && !end); endIndex++)
    {
      char c = afterDoubleColon.charAt(endIndex);
      if (allowPseudoClass)
        end = Character.isWhitespace(c);
      else
        end = _isStyleClassTerminator(c);
    }

    String afComponentSelector = null;
    if (end)
    {
      afComponentSelector =
        singleAfSelector.substring(0, colonIndex + endIndex-1);
    }
    else
    {
      // there was no end, so just take the entire string.
      afComponentSelector = singleAfSelector;
    }

    return afComponentSelector;
  }

  // Converts all style class selectors that begin with '.'
  // to the short version if
  // there is a short version. does not shorten styles that start with the
  // namespace
  // returns null if it can't shorten the selector
  private static String _getShortNonNamespacedSelector(
    String              selector,
    Map shortStyleClassMap)
  {
    if (shortStyleClassMap == null)
      return null;

    // This will see if the selector is a single styleClass, and if so,
    // it will run it through the shortStyleClassMap and return the shortened
    // style class. A single style class selector is something like
    // ".AFInstructionText". Examples that are not single style class
    // selectors are "af|inputText" or ".AFFoo .AFBar" or ".foo:hover"
    if (isSingleStyleClassSelector(selector))
    {
      String shortStyleClass = shortStyleClassMap.get(selector.substring(1));

      return (shortStyleClass == null) ? null : "." + shortStyleClass;
    }

    // This will run if we do not have a one class selector definition
    // but a af|XXX class or a command class like .AFXYZ .AFzzz or
    // ".foo:hover" It shortens all the .xxx pieces.
    boolean isShorter = false;
    int length = selector.length();
    StringBuffer buffer = new StringBuffer(length);
    int styleClassStartIndex = -1;

    // loop through each character. If we don't come across a '.' character,
    // return the selector unchanged.
    for (int i = 0; i < length; i++)
    {
      char c = selector.charAt(i);

      if (styleClassStartIndex == -1)
      {
        if (c == '.')
          styleClassStartIndex = i;

        buffer.append(c);
      }
      else
      {
        // We are currently inside of a style class substring.
        // Check to see if the style class has been terminated.
        boolean end = _isStyleClassTerminator(c);
        if (!end)
        {
          // Check to see if we are at the end of the string
          if (i == (length - 1))
          {
            // We need to increment our counter to make sure
            // we include this last character when we pull out
            // the substring
            i++;
            end = true;
          }
        }

        if (end)
        {

          // If we are at the end of the style class, check to
          // see if we've got a shorter version
          String styleClass = selector.substring(styleClassStartIndex + 1, i);
          String shortStyleClass = null;
          // don't shorten the styles that contain a namespace, because we
          // do this in another step.
          if (styleClass.indexOf('|') == -1)
            shortStyleClass = shortStyleClassMap.get(styleClass);


          if (shortStyleClass == null)
          {
            buffer.append(styleClass);
          }
          else
          {
            buffer.append(shortStyleClass);
            isShorter = true;
          }

          // Don't forget the terminator character
          if (i < (length - 1))
            buffer.append(c);
          // if c is ., then this means we've found another styleclass
          if (c == '.')
            styleClassStartIndex = i;
          else
            styleClassStartIndex = -1;
        }
      }
    }

    // return the original selector if this isn't shorter.
    return isShorter ? buffer.toString() : selector;
  }
  
  /**
   * Runs a selector through a map. It returns the selector unchanged (except for converted
   * pseudo-classes) if there is no namespace in the selector.
   * This could be a map to convert the
   * public no-html selector to an internal selector that has html-specifics
   * (e.g., 'af|menuPath::step:hover' -> 'af|menuPath A:hover')
   * or it could be a map to shorten the selector
   * (e.g., 'af|menuPath A:hover' -> '.x11 A:hover')
   * We call this method first with the public->internal map, and then
   * to shorten it.
   * Only the pieces of the selector that start with the namespace are mapped.
   * @param afSelectorMap if shortenPass is true, then this map shortens the
   *                 af| selector. else, it maps the public af| selector
   *                 to the internal selector (a selector that is closer to what is written to the
*                    CSS file. 
*                    e.g., af|inputText:error::content becomes 
*                    af|inputText.p_AFError af|inputText::content
   * @param namespacePrefixArray   most likely, "af|". The selectors with this namespace
   *                               are the ones we map.
   * @param selector    selector to map.
   */
  public static String getMappedSelector (
    Map afSelectorMap,
    String[]            namespacePrefixArray,
    String              selector)
  {
    String mappedSelector;
    if (_hasNamespacePrefix(selector, namespacePrefixArray))
    {
      String[] selectorArray =
        _orderPseudoElementsAndClasses(selector);

      // map selectors, if needed
      // any of the selectors that start with a namespace prefix will
      // be mapped.
      // e.g., "af|menuPath::step" maps to "af|menuPath A"
      // if the selector does not need to be 'mapped', it will be returned
      // untouched. e.g., .AFInstructionText maps to .AFInstructionText
      mappedSelector = _getMappedNSSelector(afSelectorMap,
                                            namespacePrefixArray,
                                            selector,
                                            selectorArray,
                                            false);
    }
    else
    {
      // there are no namespaces in this selector, but we still need to convert pseudo-classes.
      //Separate pseudoclasses and then call it
      int start = 0;
      StringBuilder b = new StringBuilder();
      for(int i = 0; i < selector.length(); i++)
      {
        char c = selector.charAt(i);
        if (c == ' ' )
        {
          if (start == i)
          {
            //group of spaces
            start = i+1; //Skip space
          }
          else
          {
            String subSelector = selector.substring(start,i);
            subSelector = _convertPseudoClassesInSelector(subSelector);
            start = i+1; //Skip space
            b.append(subSelector);
            b.append(' ');
          }
        }
      } // end for loop

      // We reached the end of the selector, now convert the last bit
      if (start == 0)
      {
        //there is no space in selector, but we still need to map pseudo-classes.
        mappedSelector = _convertPseudoClassesInSelector(selector);
      }
      else
      {
        String subSelector = selector.substring(start);
        subSelector = _convertPseudoClassesInSelector(subSelector);
        b.append(subSelector);
        mappedSelector = b.toString();
      }

    }
    return mappedSelector;
  }

  /**
   * Runs a namespaced selector through a map.
   * This could be a map to convert the
   * public no-html selector to an internal selector that has html-specifics
   * (e.g., 'af|menuPath::step:hover' -> 'af|menuPath A:hover')
   * or it could be a map to shorten the selector
   * (e.g., 'af|menuPath A:hover' -> '.x11 A:hover')
   * We call this method first with the public->internal map, and then
   * to shorten it.
   * Only the pieces of the selector that start with the namespace are mapped.
   * @param map         if shortenPass is true, then this map shortens the
   *                    af| selector. else, it maps the public af| selector
   *                    to the internal selector.
   * @param namespace   most likely, "af|". The selectors with this namespace
   *                    are the ones we map.
   * @param selector    selector to map.
   * @param selectorArray selectorArray is the selector split into pieces based on the ' '
   * @param shorten     if true, then we'll add the "." to the mapped selector.
   * @return            the selector, mapped.
   */
  private static String _getMappedNSSelector (
    Map map,
    String[]            nsPrefixArray,
    String              selector,
    String[]            selectorArray,
    boolean             shorten)
  {
    // selectorArray is the selector broken into pieces.
    // Map each piece, then put back together.

    for (int i=0; i < selectorArray.length; i++)
    {
      // Assumption is the selector 'piece' only contains one namespace
      // prefix. af|foo.tr|bar is not a valid selector, so this assumption
      // is fine.
      int nsIndex = -1;
      int numNsPrefixes = nsPrefixArray.length;
      for (int j=0; (j <  numNsPrefixes) && (nsIndex == -1); j++)
      {
       nsIndex = selectorArray[i].indexOf(nsPrefixArray[j]);
      }

      if (nsIndex > -1)
      {
        selectorArray[i] = _getEachMappedSelector(map,
                                                  nsIndex,
                                                  selectorArray[i],
                                                  shorten);
      }
    }

    // build back the selectorArray into a string
    return _arrayToStringWithSpaces(selectorArray);
  }

  private static String _getEachMappedSelector(
    Map map,
    int                 indexOfNSPrefix,
    String              selector,
    boolean             shorten)
  {
    // break apart the selector into 2 parts:
    // main (af|foo::pseudo-element)
    // end (anything after, ':' or including and after '.')
    // map the main and the ending (pseudo-classes could be mapped to private
    // styleclasses, :disabled -> .p_AFDisabled
    // piece  back together.
    if (indexOfNSPrefix == -1)
     return selector;
    if (map == null)
      return selector;


    String wholeAfSelector = selector.substring(indexOfNSPrefix);

    // get main piece
    int colonIndex = wholeAfSelector.indexOf("::");

    // get the part after the ::.
    // colonIndex will be 0 if there is no ::.
    if (colonIndex != -1)
    {
      if (_LOG.isWarning() &&
          (wholeAfSelector.lastIndexOf("::") != colonIndex))
      {
        _LOG.warning("UNSUPPORTED_CONSECUTIVE_SUB_ELEMENT_SYNTAX", selector);
      }

      colonIndex += 2;
    }
    else
      colonIndex = 0;
    String afterDoubleColon = wholeAfSelector.substring(colonIndex);

    // now find the part with a single ':', a ' ', a '[', or a '.'. That is where
    // I want to chop it to get my 'main' piece of the component selector.
    boolean end = false;
    int afterLength = afterDoubleColon.length();
    int endIndex=0;
    char c;
    for (; ((endIndex < afterLength) && !end); endIndex++)
    {
      c = afterDoubleColon.charAt(endIndex);
      end = (Character.isWhitespace(c)) || (c == '.') || (c == ':') || (c == '[');
    }

    // Set the main piece in the pieces object
    String mainSelector;
    if (endIndex == afterLength)
    {
      mainSelector = wholeAfSelector.substring(0, colonIndex + endIndex);
    }
    else
    {
      mainSelector = wholeAfSelector.substring(0, colonIndex + endIndex-1);
    }
    String afterMain = null;
    if (endIndex != afterLength)
    {
      // If I got here, that means there are characters after the 'main' part
      // of the selector.
      afterMain = wholeAfSelector.substring(colonIndex + endIndex-1);
      // map the afterMain part. It includes styleclasses and pseudo-classes.
      // we don't need to convert the pseudo-classes if we are shortening.
      if (!shorten)
        afterMain = _convertPseudoClassesInSelector(afterMain);
    }

    // piece back together the string
    StringBuffer buffer = new StringBuffer();

    // beginning. add '.' if needed.
    if (indexOfNSPrefix > 0)
    {
      buffer.append(selector.substring(0, indexOfNSPrefix));
      if (shorten && selector.charAt(indexOfNSPrefix-1) != '.')
        buffer.append('.');
    }
    else if (shorten)
      buffer.append('.');

    // map the mainSelector, and append the afterMain part.
    buffer.append(_runThroughMap(map, mainSelector));
    if (afterMain != null)
      buffer.append(afterMain);


    return buffer.toString();
  }

  private static boolean _hasNamespacePrefix(
    String   selector,
    String[] nsPrefixArray)
  {
    if (selector == null) return false;
    boolean hasNamespacePrefix = false;
    int numNamespaces = nsPrefixArray.length;
    for (int i=0; (i <  numNamespaces )&& !hasNamespacePrefix; i++)
      if (selector.indexOf(nsPrefixArray[i]) > -1)
        hasNamespacePrefix = true;
    return hasNamespacePrefix;
  }

  /*
   * Returns a string representation of the contents of the specified array
   * where adjacent elements are separated a space (" ").
   */
  private static String _arrayToStringWithSpaces(
    String[] stringArray
    )
  {
    int length = stringArray.length;
    // if only one thing in the array, just return it to save some time.
    if (stringArray.length == 1) return stringArray[0];

    // get the bufferSize
    int bufferSize = 0;
    for (int i=0; i < length; i++)
    {
      bufferSize += stringArray[i].length() + 1;
    }
    // turn the array into a space deliminated String
    StringBuffer returnString = new StringBuffer(bufferSize);
    for (int i=0; i < length; i++)
    {
      returnString.append(stringArray[i]);
      if (i+1 < length)
        returnString.append(' ');
    }
    return returnString.toString();
  }

  /*
   * if I see this: af|foo:class:class::element, return this
   * af|foo:class:class and af|foo::element in a String array.
   * if I see this: af|foo:p-class.StyleClass::element, return this:
   * 'af|foo:p-class.StyleClass' 'af|foo::element'
   * af|foo.StyleClass::element -> 'af|foo.StyleClass' 'af|foo::element'
   * If I see thiss: af|foo::p-element.StyleClass, return this
   * af|foo::p-element.StyleClass (leave alone).
   */
  private static String[]  _orderPseudoElementsAndClasses(
    String selector)
  {
    String[] input = _splitStringByWhitespace(selector);

    List output = new ArrayList();
    for (int i=0; i < input.length; i++)
    {

      int indexOfDoubleColon = input[i].indexOf("::");
      if (indexOfDoubleColon == -1)
      {
        // no double colon (aka pseudo-element)
        output.add(input[i]);
      }
      else
      {
        // you have a double colon index. Now look to see if we need to
        // reorder. We have to reorder if the pseudo-element comes after
        // the pseudo-class or composite style classes.
        int indexOfFirstColon = input[i].indexOf(':');
        int indexOfDot = input[i].indexOf('.');

        boolean pseudoClassBeforePseudoElement =
          (indexOfFirstColon < indexOfDoubleColon);
        boolean styleClassBeforePseudoElement =
          (indexOfDot != -1 && indexOfDot < indexOfDoubleColon);

        if (!(pseudoClassBeforePseudoElement ||
              styleClassBeforePseudoElement))
        {
          output.add(input[i]);
        }
        else
        {
          if (indexOfFirstColon == indexOfDoubleColon)
            indexOfFirstColon = -1;
          int indexOfClass = Math.min(indexOfFirstColon, indexOfDot);
          if (indexOfClass == -1)
            indexOfClass = Math.max(indexOfFirstColon, indexOfDot);

          // we have the condition where pseudo-class or styleClass is before
          // pseudo-element: af|foo:psdo-class::psdo-element
          // e.g.,
          // af|inputText:disabled::content
          // indexOfColon = 12, indexOfDoubleColon = 21
          // main = 'af|inputText'
          // mainPlusClasses = 'af|inputText:disabled'
          // end '::content'
          String main = input[i].substring(0, indexOfClass);
          String mainPlusClasses = input[i].substring(0, indexOfDoubleColon);
          String end = input[i].substring(indexOfDoubleColon);
          output.add(mainPlusClasses);
          output.add(main + end);
        }
      }

    }
    return output.toArray(new String[output.size()]);
  }



  /**
   * get rid of the | and :: that browsers don't like, and add the
   * '.' where needed to make the af| component selector
   * into a style class selector that is valid to be written to the css file.
   * @param selector
   * @return
   */
  public static String getValidFullNameSelector(
    String selector,
    String[] namespacePrefixArray)
  {
    if (selector.indexOf('|') == -1)
      return selector;

    // split on spaces.
    String[] spacerArray = _splitStringByWhitespace(selector);

    for (int i=0; i < spacerArray.length; i++)
    {
      // if this starts with any of our namespaces, then add a '.'
      if (spacerArray[i].indexOf('|') > -1)
      {
        for (int j=0; j < namespacePrefixArray.length; j++)
        {
          String nsPrefix = namespacePrefixArray[j];
          if (spacerArray[i].startsWith(nsPrefix))
          {
            spacerArray[i] = ".".concat(spacerArray[i]);
            break;
          }
        }
      }
    }
    return StyleUtils.convertToValidSelector(
      _arrayToStringWithSpaces(spacerArray));

  }


  // Tests whether the specified character or pseudo-class terminates a
  // style class selector
  private static boolean _isStyleClassTerminator(char c)
  {
    return (Character.isWhitespace(c) || (c == ':') || (c == '.') || (c == '['));
  }

  // Gets the properties of the specified StyleNode in sorted
  // order as a String.
  private static String _getSortedPropertyString(StyleNode style)
  {
    // First, pull the properties out of the StyleNode
    // -= Simon Lessard =-
    // TODO: Check if synchronization is needed, otherwise uses
    //       an ArrayList instead. Even if synchronization is needed
    //       Collections.synchronizedList(ArrayList) would probably be
    //       a better choice.
    Vector v = new Vector();
    Iterable propertyNodeList = style.getProperties();
    for (PropertyNode propertyNode : propertyNodeList)
      v.addElement(propertyNode);

    PropertyNode[] properties = new PropertyNode[v.size()];
    v.copyInto(properties);

    // Sort the properties so that the order of the properties won't
    // come into play when comparing property strings.
    Arrays.sort(properties,PropertyNodeComparator.sharedInstance());

    // Compute the StringBuffer size
    int bufferSize = 0;
    for (int i = 0; i < properties.length; i++)
    {
      PropertyNode property = properties[i];
      String name = property.getName();
      String value = property.getValue();

      if ((name != null) && (value != null))
      {
        bufferSize += property.getName().length();
        bufferSize += property.getValue().length();

        // Leave room for a separator
        bufferSize++;
      }
    }

    StringBuffer buffer = new StringBuffer(bufferSize);
    boolean first = true;

    for (int i = 0; i < properties.length; i++)
    {
      PropertyNode property = properties[i];
      String name = property.getName();
      String value = property.getValue();

      if ((name != null) && (value != null))
      {
        if (!first)
          buffer.append(";");
        else
          first = false;

        buffer.append(name);
        buffer.append(":");
        buffer.append(value);
      }
    }

    return buffer.toString();
  }

  /**
   * Given a selector, convert the pseudo-classes. Non css-2 pseudo-classes
   * get converted to styleclasses so they don't get passed through to the generated css.
   * @param selector String input selector. The assumption is there are no spaces.
   * If the original selector has spaces, then break the selector up into the pieces before
   * calling this method.
   * @return String the selector with the pseudo-classes converted, if needed.
   * e.g., .StyleClass:error:active -> .StyleClass.p_AFError:active
   */
  private static String _convertPseudoClassesInSelector(String selector)
  {
    if (selector == null) return selector;

    StringBuffer completeBuffer = new StringBuffer();
    StringBuffer pseudoClassBuffer = new StringBuffer();
    boolean inPseudoClass = false;

    for (int i=0; i < selector.length(); i++)
    {
      char x = selector.charAt(i);

      if ((x == ':') || (x == '.') || (x == '['))
      {
        if (inPseudoClass)
        {
          // if we are in a pseudo-class already, and we get a ':' or '.' that means
          // this pseudo-class is complete. Get ready for another one.
          String convertedPseudoClass = _convertPseudoClass(pseudoClassBuffer.toString());
          completeBuffer.append(convertedPseudoClass);
          pseudoClassBuffer = new StringBuffer();
          inPseudoClass = false;
        }
        if (x == ':')
        {
          inPseudoClass = true;
          pseudoClassBuffer.append(x);
        }
        else if (x == '.' || x == '[')
        {
          completeBuffer.append(x);
        }

      }
      else
      {
        if (!inPseudoClass)
          completeBuffer.append(x);
        else
          pseudoClassBuffer.append(x);
      }

    }
    if (inPseudoClass)
    {
      String mappedPseudoClass = _convertPseudoClass(pseudoClassBuffer.toString());
      completeBuffer.append(mappedPseudoClass);

    }
    return completeBuffer.toString();
  }

  /**
   * return the array of strings computed by splitting this string
   * around one or more whitespaces This calls Character.isWhitespace to determine if it is
   * whitespace. This is important because tabs, newlines, etc count as whitespace
   * in the selector strings. This is faster than String's split("\\s")
   * @param selector
   * @return String[] The array of Strings computed by splitting the input String
   * around one or more spaces. e.g, "af|foo    af|bar" returns "af|foo" and "af|bar" in
   * a String Array.
   */
  private static String[] _splitStringByWhitespace (
    String  selector)
  {
    // return a String[] with each piece that is deliminated by the inChar.
    int length = selector.length();
    StringBuffer buffer = new StringBuffer(length);
    List splitList = new ArrayList();
    boolean inWhitespace = false;

    for (int i=0; i < length; i++)
    {
      char c = selector.charAt(i);
      if (Character.isWhitespace(c))
      {
        // we hit the whitespace delimiter, so put it in the splitList and start a new buffer.
        // ignore spaces that are in a row
        if (!inWhitespace)
        {
          String bufferString = buffer.toString();
          if (bufferString.length() > 0)
          {
            splitList.add(bufferString);
            buffer = new StringBuffer(length);
            inWhitespace = true;
          }
        }
      }
      else
      {
        buffer.append(c);
        if (inWhitespace)
          inWhitespace = false;
      }
    }
    // we are done with all the characters
    String lastString = buffer.toString();
    if (lastString.length() > 0)
      splitList.add(lastString);

    return splitList.toArray(_EMPTY_STRING_ARRAY);

  }

  // run through the map. If it's not in the map, return the selector unchanged.
  private static String _runThroughMap(Map map, String selector)
  {
    String mappedSelector = map.get(selector);
    return (mappedSelector != null) ? mappedSelector : selector;
  }


  // Comparator that sorts PropertyNodes by name
  private static class PropertyNodeComparator implements Comparator
  {
    public static Comparator sharedInstance()
    {
      return _sInstance;
    }

    public int compare(PropertyNode o1, PropertyNode o2)
    {
      String name1 = (o1 == null) ? null : o1.getName();
      String name2 = (o2 == null) ? null : o2.getName();

      if ((name1 == null) || (name2 == null) )
      {
        if (name1 == name2)
          return 0;
        else if (name1 != null)
          return 1;

        return -1;
      }

      return name1.compareTo(name2);
    }

    private PropertyNodeComparator() {}

    private static final Comparator _sInstance =
      new PropertyNodeComparator();
  }

  static private String _convertPseudoClass(String pseudoClass)
  {
    // The design time needs the browser-supported pseudo-classes to be converted so they
    // can show a preview of the skinned component.
    if (_BUILT_IN_PSEUDO_CLASSES.contains(pseudoClass) && !Beans.isDesignTime())
      return pseudoClass;
    StringBuilder builder = new StringBuilder(pseudoClass.length() + 3);
    builder.append(".");
    builder.append(SkinSelectors.STATE_PREFIX);

    for (String content : _DASH_PATTERN.split(pseudoClass.substring(1)))
    {
      if (content.length() > 0)
      {
        builder.append(Character.toUpperCase(content.charAt(0)));
        builder.append(content.substring(1));
      }
    }

    return builder.toString();
  }

  // We want to output to the css the browser-supported pseudo-classes as is
  static private final Set _BUILT_IN_PSEUDO_CLASSES =
    new HashSet();
  static
  {
    _BUILT_IN_PSEUDO_CLASSES.add(":first-child");
    _BUILT_IN_PSEUDO_CLASSES.add(":link");
    _BUILT_IN_PSEUDO_CLASSES.add(":visited");
    _BUILT_IN_PSEUDO_CLASSES.add(":hover");
    _BUILT_IN_PSEUDO_CLASSES.add(":active");
    _BUILT_IN_PSEUDO_CLASSES.add(":focus");
  }

  private static final Pattern _DASH_PATTERN =  Pattern.compile("-");
  private static final String[] _EMPTY_STRING_ARRAY = new String[0];
  private static final int _MSIE_SELECTOR_LIMIT = 4095;

  private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(CSSGenerationUtils.class);
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy