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

com.codename1.ui.html.CSSParser Maven / Gradle / Ivy

There is a newer version: 7.0.164
Show newest version
/*
 * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores
 * CA 94065 USA or visit www.oracle.com if you need additional information or
 * have any questions.
 */
package com.codename1.ui.html;

import com.codename1.xml.ParserCallback;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Vector;

/**
 * A parser for CSS segments or files.
 * Note that this class is not derived from XMLParser or HTMLParser as CSS format is significantly different than XML/HTML
 *
 * @author Ofir Leitner
 */
class CSSParser {

   /**
    * The supported CSS media types, this is relevant for CSS at-rules (i.e. @import and @media)
    * The default values according to the WCSS specs the default one is "handheld" and "all" (Which is always accepted)
    */
   private static String[] SUPPORTED_MEDIA_TYPES = {"all","handheld"};

   private static CSSParser instance;

   private CSSParserCallback parserCallback;
   
    /**
     * Returns or creates the Parser's single instance
     *
     * @return the Parser's instance
     */
    static CSSParser getInstance() {
        if (instance==null) {
            instance=new CSSParser();
        }
        return instance;
    }

    /**
     * Sets the supported CSS media types to the given strings.
     * Usually the default media types ("all","handheld") should be suitable, but in case this runs on a device that matches another profile, the developer can specify it here.
     *
     * @param supportedMediaTypes A string array containing the media types that should be supported
     */
    static void setCSSSupportedMediaTypes(String[] supportedMediaTypes) {
        SUPPORTED_MEDIA_TYPES=supportedMediaTypes;
    }


    // ***********
    // CSS Parsing methods from here onward
    // ***********

    /**
     * Checks if the specified character is a white space or not.
     * Exposed to packaage since used by HTMLComponent as well
     *
     * @param ch The character to check
     * @return true if the character is a white space, false otherwise
     */
    static boolean isWhiteSpace(char ch) {
        return ((ch==' ') || (ch=='\n') || (ch=='\t') || (ch==10) || (ch==13));
    }


    /**
     * Handles a CSS comment segment
     *
     * @param r The stream reader
     * @return The next char after the comment
     * @throws IOException
     */
    private char handleCSSComment(ExtInputStreamReader r) throws IOException {
        char c= r.readCharFromReader();
        if (c=='*') {
            char lastC='\0';
            while ((c!='/') || (lastC!='*')) {
                lastC=c;
                c= r.readCharFromReader();
            }
            c= r.readCharFromReader();
            while(((byte)c) != -1 && isWhiteSpace(c)) { //skip white spaces
                c= r.readCharFromReader();
            }
        } else {
            r.unreadChar(c);
            return '/';
        }
        return c;
    }

    /**
     * Reads the next CSS token from the reader
     *
     * @param r The stream reader
     * @param readNewline true to read new lines and not break when they're found, false otherwise
     * @param ignoreCommas true to ignore commas and not break when they're found, false otherwise
     * @param ignoreColons true to ignore colons and not break when they're found, false otherwise
     * @param ignoreWhiteSpaces true to ignore white spaces and not break when they're found, false otherwise
     * @return The next CSS token
     * @throws IOException
     */
    private String nextToken(ExtInputStreamReader r, boolean readNewline,boolean ignoreCommas,boolean ignoreColons,boolean ignoreWhiteSpaces) throws IOException {
        boolean newline = false;
        StringBuilder currentToken = new StringBuilder();
        char c= r.readCharFromReader();

        // read the next token from the CSS stream
        while(((byte)c) != -1 && isWhiteSpace(c)) {
            newline = newline || (c == 10 || c == 13 || c == ';' || ((c == ',') && (!ignoreCommas)) || (c == '>') || (c == '+'));
            if(!readNewline && newline) {
                return null;
            }
            c= r.readCharFromReader();
        }
        if (c==';' && readNewline) { //leftover from compound operation
            c= r.readCharFromReader();
            while(((byte)c) != -1 && isWhiteSpace(c)) { // This was added since after reading ; there might be some more white spaces. However there needs to be a way to combine this with the previous white spaces code or with the revised newline detection and unreading char below
                newline = newline || (c == 10 || c == 13 || c == ';' || ((c == ',') && (!ignoreCommas))  || (c == '>') || (c == '+'));
                c= r.readCharFromReader();
            }


        }
        char segment='\0'; // segment of (...) or "..." or '...'
        while(((byte)c) != -1 && ((!isWhiteSpace(c)) || (segment != '\0') || (ignoreWhiteSpaces)) && c != ';' && ((c != ':') ||
               (segment!='\0') || (ignoreColons))  && ((c != ',') || (segment != '\0') ||
               (ignoreCommas)) && (((c != '>') && (c != '+')) || (segment != '\0'))) { //- : denotes pseudo-classes, would like to keep them as one token

            if ((segment=='\0') && (c=='/')) { //comment start perhaps, if inside brackets - ignore
                c=handleCSSComment(r);
            }

            if ((c == '}' || c == '{' || c == '*' ) && (segment=='\0')) { //enter only if not in the middle of a segment. i.e. '*N'
                newline = true;
                if(currentToken.length() == 0) {
                    if(!readNewline) {
                        r.unreadChar(c);
                        return null;
                    }
                    return "" + c;
                }
                r.unreadChar(c);
                break;
            }
            currentToken.append(c);

            if (c=='(') {
                segment=')';
            } else if ((segment=='\0') && ((c=='\"') || (c=='\''))) { //Note - This keeps track of one segment only, while in fact there can be "nested" segments - i.e. ("...") which is common in URLs, though not sure it is critical as such pattern works correctly even now
                segment=c;
            } else if (c==segment) {
                segment='\0';
            }
            c= r.readCharFromReader();
        }
        if (((c==',') && (!ignoreCommas)) || (c=='>') || (c=='+')) {
            currentToken.append(c);
        }

        if((!readNewline) && (c==';')  && (currentToken.length() != 0) ) {
            r.unreadChar(c);
        }

        if(currentToken.length() == 0) {
            return null;
        }
        return currentToken.toString();
    }

    /**
     * Copies all attributes from
     *
     * @param element The element to copy from
     * @param selectors A vector containing grouped selectors to copy the attributes to
     * @param addTo The main element to add the grouped selectors to
     */
    private void copyAttributes(CSSElement element,Vector selectors,HTMLElement addTo) {
        if (selectors==null) {
            return;
        }
        for(Enumeration e=selectors.elements();e.hasMoreElements();) {
            CSSElement selector=(CSSElement)e.nextElement();
            addTo.addChild(selector);
            while (selector.getNumChildren()>0) { // This makes sure we get the last nested selector
                selector=selector.getCSSChildAt(0);
            }
            element.copyAttributesTo(selector);
        }
    }

    /**
     * Returns true if the specified CSS media type is unsupported, false otherwise
     *
     * @param media A string identifying the media type (i.e. "handheld")
     * @return true if the specified CSS media type is uspported, false otherwise
     */
    private boolean isMediaTypeSupported(String media) {
        for(int i=0;i0) {
            c= r.readCharFromReader();
            if ((((byte)c)==-1)) {
                break; //end of file
            }
            if (match) {
                segment.append(c);
            }
            if (c=='{') {
                count++;
            } else if (c=='}') {
                count--;
            }
        }

        if (match) {
            ExtInputStreamReader segmentReader=null;
            if (encoding!=null) {
                try {
                    segmentReader=new ExtInputStreamReader(new InputStreamReader(new ByteArrayInputStream(segment.toString().getBytes()),encoding));
                } catch (UnsupportedEncodingException uee) {
                    notifyError(ParserCallback.ERROR_ENCODING, "@media", null, encoding, "Encoding '"+encoding+"' failed for media segment. "+uee.getMessage());
                }
            }
            if (segmentReader==null) { //either no encoding, or encoding failed
                segmentReader=new ExtInputStreamReader(new InputStreamReader(new ByteArrayInputStream(segment.toString().getBytes())));
            }
            return segmentReader;
        } else {
            return null;
        }

    }

    /**
     * Reads a CSS file/stream and returns the tokenized CSS as a single level element tree with the
     * root appearing as a "style".
     * This method is called upon finding linked/external CSS and embedded CSS segments.
     * It handles at-rules such as import/charset/media and forwards relevant segments to the parseCSS method
     *
     * @param isr The Reader representing the stream
     * @param is The InputStream representing the stream (We need it too, in case encoding changes and we need to create another InputStreamReader)
     * @param htmlC The HTMLComponent
     * @param  pageURL For external CSS the URL of the CSS, for embedded - null
     * @return A CSSElement containing all selectors found in the stream as its children
     * @throws IOException on input stream failure
     */
    CSSElement parseCSSSegment(Reader isr,InputStream is,HTMLComponent htmlC,String pageURL) throws IOException {
        CSSElement addTo = new CSSElement("style");
        ExtInputStreamReader r = new ExtInputStreamReader(isr);
        DocumentInfo docInfo=null;
        String encoding=htmlC.getDocumentInfo()!=null?htmlC.getDocumentInfo().getEncoding():null;
        String token = nextToken(r,true,false,true,false);
        while(token.startsWith("@")) {
            if (token.equals("@import")) {
                token = nextToken(r,true,true,true,true);
                String url=getImportURLByMediaType(token);
                if (url!=null) {
                    if (docInfo==null) {
                        docInfo=pageURL==null?htmlC.getDocumentInfo():new DocumentInfo(pageURL);
                    }
                    if (docInfo!=null) {
                        htmlC.getThreadQueue().addCSS(docInfo.convertURL(url),encoding); // Referred CSS "inherit" charset from the referring document
                    } else {
                        if (DocumentInfo.isAbsoluteURL(url)) {
                            htmlC.getThreadQueue().addCSS(url,encoding); // Referred CSS "inherit" charset from the referring document
                        } else {
                            notifyError(CSSParserCallback.ERROR_CSS_NO_BASE_URL, "@import", null, url, "Ignoring CSS file referred in an @import rule ("+url+"), since page was set by setBody/setHTML/setDOM so there's no way to access relative URLs");
                        }
                    }
                }
            } else if (token.equals("@media")) {
                ExtInputStreamReader mediaReader = getMediaSegment(r,encoding,htmlC);
                if (mediaReader!=null) {
                    parseCSS(mediaReader, htmlC, addTo,null);
                }
            } else if (token.equals("@charset")) {
                token = CSSEngine.omitQuotesIfExist(nextToken(r,true,false,true,false));
                if (is!=null) { // @charset applies only to external style sheet, and the inputstream is null for embedded CSS segments
                    try {
                        ExtInputStreamReader encodedReader=new ExtInputStreamReader(new InputStreamReader(is, token));
                        r=encodedReader;
                        encoding=token;
                    } catch (UnsupportedEncodingException uee) {
                        notifyError(ParserCallback.ERROR_ENCODING, "@charset", null, token, "External CSS encoding @charset "+token+" directive failed: "+uee.getMessage());
                    }
                }
            }
            token = nextToken(r,true,false,true,false);
        }

        return parseCSS(r, htmlC,  addTo,token);
    }

    /**
     * Reads a CSS file/stream and returns the tokenized CSS as a single level element tree with the
     * root appearing as a "style".
     * This method is called either directly on style attributes.
     *
     * @param r The stream reader containing the CSS segment
     * @param htmlC The HTMLComponent
     * @return A CSSElement containing all selectors found in the stream as its children
     * @throws IOException on input stream failure
     */
    CSSElement parseCSS(InputStreamReader r,HTMLComponent htmlC) throws IOException {
        ExtInputStreamReader er=new ExtInputStreamReader(r);
        return parseCSS(er, htmlC, null,null);
    }

    /**
     * Reads a CSS file/stream and returns the tokenized CSS as a single level element tree with the
     * root appearing as a "style".
     *
     * @param r The stream reader containing the CSS segment
     * @param htmlC The HTMLComponent
     * @param addTo the master CSSElement to add the selectors to (or null to open a new one_
     * @param firstToken A first toekn to process, or null if none
     * @return A CSSElement containing all selectors found in the stream as its children
     * @throws IOException on input stream failure
     */
    CSSElement parseCSS(ExtInputStreamReader r,HTMLComponent htmlC,CSSElement addTo,String firstToken) throws IOException {
        if (addTo==null) {
            addTo = new CSSElement("style");
        }
        CSSElement parent = addTo;
        Vector selectors = new Vector();
        CSSElement lastGroupedParent=null;

        boolean selectorMode = true;
        boolean grouping=false; // Grouping is when selector are grouped, i.e. h1,h2,h3 { ... }
        boolean childSelector=false; // when 'a > b' appears it is a child seelctor, meaning only 'b' that is a direct child of 'a' (unlike 'a b' which is a descendant selector)
        boolean siblingSelector=false; // when 'a + b' appears it is a sibling seelctor, meaning only 'b' that is adjacent to 'a'
        
        String token = "";

        //TODO - detect BOM for UTF8 etc.
        while(true) {

            if (firstToken!=null) {
                token=firstToken;
                firstToken=null;
            } else {
                token = nextToken(r,true,false,selectorMode,false);
            }
            if(token == null || token.indexOf(" -1) {
                break;
            }

            if("{".equals(token)) {
                selectorMode = false;
                grouping=false;
                continue;
            }
            if("}".equals(token)) {
                selectorMode = true;
                copyAttributes(parent, selectors,addTo);
                parent = addTo;
                selectors = new Vector();
                lastGroupedParent=null;
                continue;
            }

            if(selectorMode) {
                // Checks for grouped selectors, note that due to spacing the comma can either appear as a separate token, or at the start of a token or at its end
                // All these scenarios are checked in the following lines of code.
                if (",".equals(token)) {
                    grouping=true;
                    continue;
                }

                if (">".equals(token)) {
                    childSelector=true;
                    continue;
                }

                if ("+".equals(token)) {
                    siblingSelector=true;
                    continue;
                }

                if (token.startsWith(",")) {
                    token=token.substring(1);
                    grouping=true;
                } else if (token.startsWith(">")) {
                    token=token.substring(1);
                    childSelector=true;
                } else if (token.startsWith("+")) {
                    token=token.substring(1);
                    siblingSelector=true;
                }

                boolean nextIsChildSelector=false;
                boolean nextIsSiblingSelector=false;

                if (token.endsWith(">")) {
                    nextIsChildSelector=true;
                    token=token.substring(0, token.length()-1);
                } else if (token.endsWith("+")) {
                    nextIsSiblingSelector=true;
                    token=token.substring(0, token.length()-1);
                }


                if (grouping) {
                    if (token.endsWith(",")) {
                        token=token.substring(0, token.length()-1);
                    } else {
                        grouping=false; //  there was no comma at the end, so next time it is not a grouped element (unless a comma will be detected as the next token or the start of the next token)
                    }
                    CSSElement entry = new CSSElement(token);
                    selectors.addElement(entry);
                    lastGroupedParent=entry;

                } else {
                    if (token.endsWith(",")) {
                        grouping=true;
                        token=token.substring(0, token.length()-1);
                    } 
                    CSSElement entry = new CSSElement(token);

                    
                    entry.descendantSelector=!childSelector;
                    entry.siblingSelector=siblingSelector;
                    if (lastGroupedParent==null) {
                        parent.addChild(entry);
                        parent = entry;
                    } else {
                        lastGroupedParent.addChild(entry);
                        lastGroupedParent=entry;
                    }
                    
                }
                childSelector=nextIsChildSelector;
                siblingSelector=nextIsSiblingSelector;

            } else {
                boolean compoundToken = false;

                for(int iter = 0 ; iter < CSSElement.CSS_SHORTHAND_ATTRIBUTE_LIST.length ; iter++) {
                    if(CSSElement.CSS_SHORTHAND_ATTRIBUTE_LIST[iter].equals(token)) {
                            compoundToken = true;
                            boolean collattable=CSSElement.CSS_IS_SHORTHAND_ATTRIBUTE_COLLATABLE[iter];
                            int valsAdded=0;
                            token = nextToken(r, false,false,false,false);

                            // This array is used for collatable attributes - the values can't be set as they are read, first we need to see how many values appear and set accordingly
                            String[] tokens = new String[4];
                            while(token!=null) {
                                if (collattable) {
                                    if (valsAdded0)) {
                                for(int i=0;i=CSSElement.CSS_STYLE_ID_OFFSET) {

                if (!selector.isAttributeAssigned(attrIndex)) { // Only check if the attribute wasn't set yet
                    int result=selector.addAttribute(attrIndex, value);
                    if (result==-1) { //no error code return - success
                        return true;
                    }
                }
            } else {
                boolean success=addShorthandAttribute(value, attrIndex, selector);
                if (success) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Adds the specified value to the specified selector as a value to the shorthand and collatable attribute whose index is specified
     * This is called from addShorthandAttribute when a shorthand attribute maps to a collatable attribute
     * Note that while usually collatable attributes can have 1-4 values, and are mapped according to CSSElement.CSS_COLLATABLE_ORDER
     * When they are specified as part of a top shorthand attribute, only one value can be specified and it is copied to all base attributes.
     * For example, While the definition 'border-width: 5px 10px' will set the vertical border width to 5 and the horizontal to 10,
     * One cannot specify: 'border: 5px 10px solid red' - but rather has to specify only one value that will be set as the width for all sides.
     *
     * @param value The attribute's value
     * @param shorthandAttr The attribute's index
     * @param selector The selector to add the attribuet to
     * @return true if succeeded to add, false otherwise (for example invalid value)
     */
    private boolean addCollatableAttribute(String value,int shorthandAttr,CSSElement selector) {
        int attrIndex=CSSElement.CSS_SHORTHAND_ATTRIBUTE_INDEX[shorthandAttr][0];
        int result=selector.addAttribute(attrIndex, value);
        if (result==-1) {
            for(int i=1;i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy