From 1b7fde7116f543ced3790276d89e3323e4c81983 Mon Sep 17 00:00:00 2001 From: Peter Bernard West Date: Tue, 7 May 2002 05:37:16 +0000 Subject: [PATCH] Initial version of experimental property expression parsing. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/FOP_0-20-0_Alt-Design@194777 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/fop/fo/expr/PropertyParser.java | 718 +++++++++++------- .../apache/fop/fo/expr/PropertyTokenizer.java | 383 +++++++--- 2 files changed, 729 insertions(+), 372 deletions(-) diff --git a/src/org/apache/fop/fo/expr/PropertyParser.java b/src/org/apache/fop/fo/expr/PropertyParser.java index 2d5a89c61..e4945c4fa 100644 --- a/src/org/apache/fop/fo/expr/PropertyParser.java +++ b/src/org/apache/fop/fo/expr/PropertyParser.java @@ -7,140 +7,195 @@ package org.apache.fop.fo.expr; -import org.apache.fop.fo.Property; -import org.apache.fop.fo.ListProperty; -import org.apache.fop.fo.LengthProperty; -import org.apache.fop.fo.NumberProperty; -import org.apache.fop.fo.StringProperty; -import org.apache.fop.fo.ColorTypeProperty; -import org.apache.fop.datatypes.*; - -import java.util.Hashtable; +import org.apache.fop.fo.PropertyConsts; +import org.apache.fop.fo.Properties; +import org.apache.fop.fo.PropNames; + +import org.apache.fop.fo.expr.PropertyValue; +import org.apache.fop.fo.expr.PropertyValueList; +import org.apache.fop.datatypes.Numeric; +import org.apache.fop.datatypes.Literal; +import org.apache.fop.datatypes.NCName; +import org.apache.fop.datatypes.Percentage; +import org.apache.fop.datatypes.Ems; +import org.apache.fop.datatypes.IntegerType; +import org.apache.fop.datatypes.Length; +import org.apache.fop.datatypes.Time; +import org.apache.fop.datatypes.Frequency; +import org.apache.fop.datatypes.Angle; +import org.apache.fop.datatypes.Bool; +import org.apache.fop.datatypes.Inherit; +import org.apache.fop.datatypes.Auto; +import org.apache.fop.datatypes.None; +import org.apache.fop.datatypes.ColorType; +import org.apache.fop.datatypes.StringType; +import org.apache.fop.datatypes.MimeType; +import org.apache.fop.datatypes.UriType; +import org.apache.fop.datatypes.FromParent; +import org.apache.fop.datatypes.FromNearestSpecified; +//import org.apache.fop.datatypes.*; + +import java.util.HashMap; /** * Class to parse XSL FO property expression. - * This class is heavily based on the epxression parser in James Clark's + * This class is heavily based on the expression parser in James Clark's * XT, an XSLT processor. + * + * PropertyParser objects are re-usable. The constructor simply creates the + * object. To parse an expression, the public method Parse is + * called. */ public class PropertyParser extends PropertyTokenizer { - private PropertyInfo propInfo; // Maker and propertyList related info - - static private final String RELUNIT = "em"; - static private final Numeric negOne = new Numeric(new Double(-1.0)); - static final private Hashtable functionTable = new Hashtable(); - - static { - // Initialize the Hashtable of XSL-defined functions - functionTable.put("ceiling", new CeilingFunction()); - functionTable.put("floor", new FloorFunction()); - functionTable.put("round", new RoundFunction()); - functionTable.put("min", new MinFunction()); - functionTable.put("max", new MaxFunction()); - functionTable.put("abs", new AbsFunction()); - functionTable.put("rgb", new RGBColorFunction()); - functionTable.put("from-table-column", new FromTableColumnFunction()); - functionTable.put("inherited-property-value", - new InheritedPropFunction()); - functionTable.put("from-parent", new FromParentFunction()); - functionTable.put("from-nearest-specified-value", - new NearestSpecPropFunction()); - functionTable.put("proportional-column-width", - new PPColWidthFunction()); - functionTable.put("label-end", new LabelEndFunction()); - functionTable.put("body-start", new BodyStartFunction()); - // NOTE: used from code generated for corresponding properties - functionTable.put("_fop-property-value", new FopPropValFunction()); - - /** - * * NOT YET IMPLEMENTED!!! - * functionTable.put("icc-color", new ICCcolorFunction()); - * functionTable.put("system-color", new SystemColorFunction()); - * functionTable.put("system-font", new SystemFontFunction()); - * - * functionTable.put("merge-property-values", new MergePropsFunction()); - */ - } - /** - * Public entrypoint to the Property expression parser. - * @param expr The specified value (attribute on the xml element). - * @param propInfo A PropertyInfo object representing the context in - * which the property expression is to be evaluated. - * @return A Property object holding the parsed result. - * @throws PropertyException If the "expr" cannot be parsed as a Property. + * This is an attempt to ensure that the restriction on the application of + * from-parent() and from-nearest-specified-value() functions to shorthand + * properties (Section 5.10.4 Property Value Functions) is maintained. + * I.e. if a shorthand property is the subject of one of these functions, + * ist is valid only if "...the expression only consists of the + * [from-parent or from-nearest-specified-value] function with an argument + * matching the property being computed..." */ - public static Property parse(String expr, PropertyInfo propInfo) - throws PropertyException { - return new PropertyParser(expr, propInfo).parseProperty(); - } - - + //private int elementsSeen = 0; /** - * Private constructor. Called by the static parse() method. - * @param propExpr The specified value (attribute on the xml element). - * @param propInfo A PropertyInfo object representing the context in - * which the property expression is to be evaluated. + * Used in conjunction with elementsSeen. */ - private PropertyParser(String propExpr, PropertyInfo pInfo) { - super(propExpr); - this.propInfo = pInfo; + //private String restrictedValueFunctSeen = null; + + public PropertyParser() { + super(); } /** * Parse the property expression described in the instance variables. + *

* Note: If the property expression String is empty, a StringProperty * object holding an empty String is returned. - * @return A Property object holding the parsed result. - * @throws PropertyException If the "expr" cannot be parsed as a Property. + * @param property an int containing the property index. + * which the property expression is to be evaluated. + * @param expr The specified value (attribute on the xml element). + * @return A PropertyValue holding the parsed result. + * @throws PropertyException If the "expr" cannot be parsed as a + * PropertyValue. */ - private Property parseProperty() throws PropertyException { - next(); - if (currentToken == TOK_EOF) { - // if prop value is empty string, force to StringProperty - return new StringProperty(""); + public PropertyValue parse(int property, String expr) + throws PropertyException + { + synchronized (this) { + // make sure this parser is available + if (expr != null) // the parser is currently active + throw new PropertyException + ("PropertyParser is currently active."); + initialize(property, expr); } - ListProperty propList = null; + + next(); + if (currentToken == EOF) + // prop value is empty + throw new PropertyException + ("No token recognized in :" + expr + ":"); + + PropertyValueList propList = new PropertyValueList(property); while (true) { - Property prop = parseAdditiveExpr(); - if (currentToken == TOK_EOF) { - if (propList != null) { - propList.addProperty(prop); + PropertyValue prop = parseAdditiveExpr(); + if (currentToken == EOF) { + // end of the expression - add to list and go + if (propList.size() != 0) { + propList.add(prop); return propList; - } else + } else { // list is empty return prop; - } else { - if (propList == null) { - propList = new ListProperty(prop); - } else { - propList.addProperty(prop); } } - // throw new PropertyException("unexpected token"); + // throw away commas separating arguments. These can occur + // in font-family and voice-family. Commas are regarded here + // as separators of list and sublist elements. + // See 7.16.5 "text-shadow" in the 1.0 Recommendation for an + // example of sublists. + if (currentToken == COMMA) { + next(); + propList.add(prop); + } else { // whitespace separates list elements; make a sublist + propList.add(parseSublist(prop)); + if (currentToken == EOF) + return propList; + } + } + } + + /** + *

Parse a property values sublist - a list of whitespace separated + * PropertyValues. + *

+ * Property value expressions for various properties may contaiin lists + * of values, which may be separated by whitespace or by commas. See, + * e.g., 7.6.17 "voice-family" and 7.8.2 "font-family". The shorthands + * may also contain lists of elements, generally (or exclusively) + * whitespace separated. 7.16.5 "text-shadow" allows whitespace + * separated length doubles or triples to be specified for individual + * shadow effects, with multiple shadow effects, each separated by + * commmas. + * @param initialValue a PropertyValue to assign as the initial + * value of the sublist. The detection of this value, which is + * whitespace separated from a subsequent value, has been the + * trigger for the creation of the sublist. + * @return a PropertyValueList containing the sublist. The + * indicatior for the end of the sublist is the end of the expression, + * or a comma. + */ + PropertyValueList parseSublist(PropertyValue initialValue) + throws PropertyException + { + PropertyValueList sublist = new PropertyValueList(property); + sublist.add(initialValue); + while (true) { + PropertyValue prop = parseAdditiveExpr(); + if (currentToken == EOF) { + // end of the expression - add to sublist and go + sublist.add(prop); + return sublist; + } + // Comma separates next element - end of sublist + if (currentToken == COMMA) { + next(); + sublist.add(prop); + return sublist; + } else { // whitespace separates next elements; add to sublist + sublist.add(prop); + } + } + } + + /** + * Reset the parser by resetting the tokenizer to null (or equivalent) + * values. + */ + public void resetParser() { + synchronized (this) { + //elementsSeen = 0; + //restrictedValueFunctSeen = null; + reset(); } - // return prop; } /** * Try to parse an addition or subtraction expression and return the - * resulting Property. + * resulting PropertyValue. */ - private Property parseAdditiveExpr() throws PropertyException { + private PropertyValue parseAdditiveExpr() throws PropertyException { // Evaluate and put result on the operand stack - Property prop = parseMultiplicativeExpr(); + PropertyValue prop = parseMultiplicativeExpr(); loop: for (; ; ) { switch (currentToken) { - case TOK_PLUS: + case PLUS: next(); - prop = evalAddition(prop.getNumeric(), - parseMultiplicativeExpr().getNumeric()); + ((Numeric)prop).add((Numeric)parseMultiplicativeExpr()); break; - case TOK_MINUS: + case MINUS: next(); - prop = - evalSubtraction(prop.getNumeric(), - parseMultiplicativeExpr().getNumeric()); + ((Numeric)prop).subtract((Numeric)parseMultiplicativeExpr()); break; default: break loop; @@ -151,27 +206,24 @@ public class PropertyParser extends PropertyTokenizer { /** * Try to parse a multiply, divide or modulo expression and return - * the resulting Property. + * the resulting PropertyValue. */ - private Property parseMultiplicativeExpr() throws PropertyException { - Property prop = parseUnaryExpr(); + private PropertyValue parseMultiplicativeExpr() throws PropertyException { + PropertyValue prop = parseUnaryExpr(); loop: for (; ; ) { switch (currentToken) { - case TOK_DIV: + case DIV: next(); - prop = evalDivide(prop.getNumeric(), - parseUnaryExpr().getNumeric()); + ((Numeric)prop).divide((Numeric)parseUnaryExpr()); break; - case TOK_MOD: + case MOD: next(); - prop = evalModulo(prop.getNumber(), - parseUnaryExpr().getNumber()); + ((Numeric)prop).mod((Numeric)parseUnaryExpr()); break; - case TOK_MULTIPLY: + case MULTIPLY: next(); - prop = evalMultiply(prop.getNumeric(), - parseUnaryExpr().getNumeric()); + ((Numeric)prop).multiply((Numeric)parseUnaryExpr()); break; default: break loop; @@ -182,12 +234,12 @@ public class PropertyParser extends PropertyTokenizer { /** * Try to parse a unary minus expression and return the - * resulting Property. + * resulting PropertyValue. */ - private Property parseUnaryExpr() throws PropertyException { - if (currentToken == TOK_MINUS) { + private PropertyValue parseUnaryExpr() throws PropertyException { + if (currentToken == MINUS) { next(); - return evalNegate(parseUnaryExpr().getNumeric()); + return ((Numeric)parseUnaryExpr()).negate(); } return parsePrimaryExpr(); } @@ -198,111 +250,286 @@ public class PropertyParser extends PropertyTokenizer { * and throws an exception if this isn't the case. */ private final void expectRpar() throws PropertyException { - if (currentToken != TOK_RPAR) + if (currentToken != RPAR) throw new PropertyException("expected )"); next(); } /** * Try to parse a primary expression and return the - * resulting Property. + * resulting PropertyValue. * A primary expression is either a parenthesized expression or an - * expression representing a primitive Property datatype, such as a + * expression representing a primitive PropertyValue datatype, such as a * string literal, an NCname, a number or a unit expression, or a * function call expression. */ - private Property parsePrimaryExpr() throws PropertyException { - Property prop; + private PropertyValue parsePrimaryExpr() throws PropertyException { + PropertyValue prop; + //if (restrictedValueFunctSeen != null) + //throw new PropertyException + //(restrictedValueFunctSeen + //+ " already seen with shorthand argument"); switch (currentToken) { - case TOK_LPAR: + case LPAR: next(); prop = parseAdditiveExpr(); expectRpar(); + // Do this here, rather than breaking, because expectRpar() + // consumes the right parenthesis and calls next(). + //elementsSeen++; return prop; - case TOK_LITERAL: - prop = new StringProperty(currentTokenValue); + case LITERAL: + prop = new Literal(property, currentTokenValue); break; - case TOK_NCNAME: + case NCNAME: // Interpret this in context of the property or do it later? - prop = new NCnameProperty(currentTokenValue); + prop = new NCName(property, currentTokenValue); break; - case TOK_FLOAT: - prop = new NumberProperty(new Double(currentTokenValue)); + case FLOAT: + // Do I need to differentiate here between floats and integers? + prop = new Numeric + (property, (new Double(currentTokenValue)).doubleValue()); break; - case TOK_INTEGER: - prop = new NumberProperty(new Integer(currentTokenValue)); + case INTEGER: + prop = IntegerType.makeInteger + (property, (new Long(currentTokenValue)).longValue()); break; - case TOK_PERCENT: + case PERCENT: /* - * Get the length base value object from the Maker. If null, then - * this property can't have % values. Treat it as a real number. + * Generate a Percentage object with the percentage number. + * The constructor converts this to a straight multiplicative + * factor by dividing by 100. */ - double pcval = - new Double(currentTokenValue.substring(0, currentTokenValue.length() - 1)).doubleValue() - / 100.0; - // LengthBase lbase = this.propInfo.getPercentLengthBase(); - PercentBase pcBase = this.propInfo.getPercentBase(); - if (pcBase != null) { - if (pcBase.getDimension() == 0) { - prop = new NumberProperty(pcval * pcBase.getBaseValue()); - } else if (pcBase.getDimension() == 1) { - prop = new LengthProperty(new PercentLength(pcval, - pcBase)); - } else { - throw new PropertyException("Illegal percent dimension value"); - } - } else { - // WARNING? Interpret as a decimal fraction, eg. 50% = .5 - prop = new NumberProperty(pcval); - } + prop = Percentage.makePercentage + (property, (new Double(currentTokenValue)).doubleValue()); break; - case TOK_NUMERIC: - // A number plus a valid unit name. - int numLen = currentTokenValue.length() - currentUnitLength; - String unitPart = currentTokenValue.substring(numLen); - Double numPart = new Double(currentTokenValue.substring(0, - numLen)); - Length length = null; - if (unitPart.equals(RELUNIT)) { - length = new Length(numPart.doubleValue(), - propInfo.currentFontSize()); - } else - length = new Length(numPart.doubleValue(), unitPart); - if (length == null) { - throw new PropertyException("unrecognized unit name: " - + currentTokenValue); - } else - prop = new LengthProperty(length); + case ABSOLUTE_LENGTH: + prop = Length.makeLength(property, + (new Double(currentTokenValue)).doubleValue(), + currentUnit); + break; + case TIME: + prop = Time.makeTime(property, + (new Double(currentTokenValue)).doubleValue(), + currentUnit); + break; + case FREQ: + prop = Frequency.makeFrequency(property, + (new Double(currentTokenValue)).doubleValue(), + currentUnit); + break; + case ANGLE: + prop = Angle.makeAngle(property, + (new Double(currentTokenValue)).doubleValue(), + currentUnit); + break; + case RELATIVE_LENGTH: + prop = Ems.makeEms(property, + (new Double(currentTokenValue)).doubleValue()); break; - case TOK_COLORSPEC: - prop = new ColorTypeProperty(new ColorType(currentTokenValue)); + case COLORSPEC: + prop = new ColorType(property, currentTokenValue); break; - case TOK_FUNCTION_LPAR: { - Function function = - (Function)functionTable.get(currentTokenValue); - if (function == null) { + case BOOL: + prop = new Bool(property, currentTokenValue); + break; + + case AUTO: + prop = new Auto(property); + break; + + case NONE: + prop = new None(property); + break; + + case INHERIT: + throw new PropertyException("INHERIT not supported"); + //break; + + case URI: + prop = new UriType(property, currentTokenValue); + break; + + case MIMETYPE: + prop = new MimeType(property, currentTokenValue); + break; + + case FUNCTION_LPAR: { + // N.B. parseArgs() invokes expectRpar at the end of argument + // processing, so, like LPAR processing, next() is not called + // and the return from this method must be premature + prop = null; + // Numeric functions + if (currentTokenValue.equals("floor")) { + PropertyValue[] args = parseArgs(1); + prop = new Numeric + (property, ((Numeric)args[0]).floor()); + } + else if (currentTokenValue.equals("ceiling")) { + PropertyValue[] args = parseArgs(1); + prop = new Numeric + (property, ((Numeric)args[0]).ceiling()); + } + else if (currentTokenValue.equals("round")) { + PropertyValue[] args = parseArgs(1); + prop = new Numeric + (property, ((Numeric)args[0]).round()); + } + else if (currentTokenValue.equals("min")) { + PropertyValue[] args = parseArgs(2); + prop = new Numeric + (property, ((Numeric)args[0]).min((Numeric)args[1])); + } + else if (currentTokenValue.equals("max")) { + PropertyValue[] args = parseArgs(2); + prop = new Numeric + (property, ((Numeric)args[0]).max((Numeric)args[1])); + } + else if (currentTokenValue.equals("abs")) { + PropertyValue[] args = parseArgs(1); + prop = new Numeric + (property, ((Numeric)args[0]).abs()); + } + + // Color functions + else if (currentTokenValue.equals("rgb")) { + PropertyValue[] args = parseArgs(3); + prop = new ColorType + (property, ((Numeric)args[0]).asInt(), + ((Numeric)args[1]).asInt(), + ((Numeric)args[2]).asInt()); + } + else if (currentTokenValue.equals("rgb-icc")) { + PropertyValue[] args = parseArgs(6); + throw new PropertyException + ("rgb-icc function is not supported."); + } + else if (currentTokenValue.equals("system-color")) { + PropertyValue[] args = parseArgs(1); + prop = new ColorType + (property, ((StringType)args[0]).getString()); + } + + // Font function + else if (currentTokenValue.equals("system-font")) { + PropertyValue[] args = parseArgs(1, 2); + throw new PropertyException + ("system-font function is not supported."); + } + + // Property value functions + else if (currentTokenValue.equals("inherited-property-value")) { + int propindex = property; + PropertyValue[] args = parseArgs(0, 1); + if (args.length != 0) + propindex = PropertyConsts.getPropertyIndex( + ((StringType)args[0]).getString()); + if (PropertyConsts.inheritance(propindex) == Properties.NO) + throw new PropertyException + ("inherited-property-value: " + + PropNames.getPropertyName(propindex) + + " is not inherited."); + prop = new Inherit(property, propindex); + } + else if (currentTokenValue.equals("label-end")) { + PropertyValue[] args = parseArgs(0); + throw new PropertyException + ("label-end function is not supported."); + } + else if (currentTokenValue.equals("body-start")) { + PropertyValue[] args = parseArgs(0); + throw new PropertyException + ("body-start function is not supported."); + } + // N.B. see comments on classes FromNearestSpecified and + // FromParent for explanation of this section + else if (currentTokenValue.equals("from-parent") || + currentTokenValue.equals("from-nearest-specified-value")) + { + // Preset the return value in case of a shorthand property + if (currentTokenValue.equals("from-parent")) + prop = new FromParent(property); + else + prop = new FromNearestSpecified(property); + + PropertyValue[] args = parseArgs(0, 1); + if (args.length == 0) { + if (! PropertyConsts.isShorthand(property)) { + // develop the function value and return it as + // a property. + throw new PropertyException + (currentTokenValue + + " function is not supported."); + } + // else a shorthand - do nothing; prop has been set + // to the appropriate pseudo-propertyValue + } else { // one argument - it must be a property name + if ( ! (args[0] instanceof NCName)) + throw new PropertyException + (currentTokenValue + " function requires" + + " property name arg."); + // else arg[0] is an NCName + NCName ncname = (NCName)args[0]; + String propname = ncname.getNCName(); + int nameindex = PropertyConsts.getPropertyIndex(propname); + if (PropertyConsts.isShorthand(nameindex)) { + // the argument is a shorthand property - + // it must be the same as the property being + // assigned to. + // see 5.10.4 Property Value Functions + if ( ! (nameindex == property)) + throw new PropertyException + (currentTokenValue + + " argument " + propname + + " does not match property " + + PropNames.getPropertyName(property)); + // else perform shorthand processing + // i.e. do nothing; prop has been set to the correct + // pseudo-propertyValue + } + else { // An NCName but not a shorthand + // Perform normal from-parent processing + throw new PropertyException + (currentTokenValue + + " function is not supported."); + } + } + } + else if (currentTokenValue.equals("from-table-column")) { + PropertyValue[] args = parseArgs(0, 1); + throw new PropertyException + ("from-table-column function is not supported."); + } + else if (currentTokenValue.equals("proportional-column-width")) { + PropertyValue[] args = parseArgs(1); + throw new PropertyException + ("proportional-column-width " + + "function is not supported."); + } + else if (currentTokenValue.equals("merge-property-values")) { + PropertyValue[] args = parseArgs(0, 1); + throw new PropertyException + ("merge-property-values function is not supported."); + } + else throw new PropertyException("no such function: " + currentTokenValue); - } - next(); - // Push new function (for function context: getPercentBase()) - propInfo.pushFunction(function); - prop = function.eval(parseArgs(function.nbArgs()), propInfo); - propInfo.popFunction(); + //elementsSeen++; return prop; } default: throw new PropertyException("syntax error"); } next(); + //elementsSeen++; return prop; } @@ -311,132 +538,53 @@ public class PropertyParser extends PropertyTokenizer { * may itself be an expression. This method consumes the closing right * parenthesis of the argument list. * @param nbArgs The number of arguments expected by the function. - * @return An array of Property objects representing the arguments - * found. - * @throws PropertyException If the number of arguments found isn't equal - * to the number expected. + * @return PropertyValueList of PropertyValue objects + * representing the arguments found. + * @exception PropertyException + */ + PropertyValue[] parseArgs(int nbArgs) throws PropertyException { + return parseArgs(nbArgs, nbArgs); + } + + /** + * Parse a comma separated list of function arguments. Each argument + * may itself be an expression. This method consumes the closing right + * parenthesis of the argument list. + * @param minArgs The minimum number of arguments expected by the function. + * @param maxArgs The maximum number of arguments expected by the function. + * @return PropertyValueList of PropertyValue objects + * representing the arguments found. N.B. The actual number of arguments + * returned is guaranteed to be between minArgs and maxArgs, inclusive, + * but the actual list of args found is terminated by the end of the + * array, or the first null element. + * @exception PropertyException */ - Property[] parseArgs(int nbArgs) throws PropertyException { - Property[] args = new Property[nbArgs]; - Property prop; + PropertyValue[] parseArgs(int minArgs, int maxArgs) + throws PropertyException + { + PropertyValue[] args = new PropertyValue[maxArgs]; + PropertyValue prop; int i = 0; - if (currentToken == TOK_RPAR) { + if (currentToken == RPAR) { // No args: func() next(); } else { while (true) { - prop = parseAdditiveExpr(); - if (i < nbArgs) { + if (i < maxArgs) { args[i++] = prop; } // ignore extra args - if (currentToken != TOK_COMMA) + if (currentToken != COMMA) break; next(); } expectRpar(); } - if (nbArgs != i) { + if (minArgs > i || i > maxArgs) { throw new PropertyException("Wrong number of args for function"); } return args; } - - /** - * Evaluate an addition operation. If either of the arguments is null, - * this means that it wasn't convertible to a Numeric value. - * @param op1 A Numeric object (Number or Length-type object) - * @param op2 A Numeric object (Number or Length-type object) - * @return A new NumericProperty object holding an object which represents - * the sum of the two operands. - * @throws PropertyException If either operand is null. - */ - private Property evalAddition(Numeric op1, - Numeric op2) throws PropertyException { - if (op1 == null || op2 == null) - throw new PropertyException("Non numeric operand in addition"); - return new NumericProperty(op1.add(op2)); - } - - /** - * Evaluate a subtraction operation. If either of the arguments is null, - * this means that it wasn't convertible to a Numeric value. - * @param op1 A Numeric object (Number or Length-type object) - * @param op2 A Numeric object (Number or Length-type object) - * @return A new NumericProperty object holding an object which represents - * the difference of the two operands. - * @throws PropertyException If either operand is null. - */ - private Property evalSubtraction(Numeric op1, - Numeric op2) throws PropertyException { - if (op1 == null || op2 == null) - throw new PropertyException("Non numeric operand in subtraction"); - return new NumericProperty(op1.subtract(op2)); - } - - /** - * Evaluate a unary minus operation. If the argument is null, - * this means that it wasn't convertible to a Numeric value. - * @param op A Numeric object (Number or Length-type object) - * @return A new NumericProperty object holding an object which represents - * the negative of the operand (multiplication by *1). - * @throws PropertyException If the operand is null. - */ - private Property evalNegate(Numeric op) throws PropertyException { - if (op == null) - throw new PropertyException("Non numeric operand to unary minus"); - return new NumericProperty(op.multiply(negOne)); - } - - /** - * Evaluate a multiplication operation. If either of the arguments is null, - * this means that it wasn't convertible to a Numeric value. - * @param op1 A Numeric object (Number or Length-type object) - * @param op2 A Numeric object (Number or Length-type object) - * @return A new NumericProperty object holding an object which represents - * the product of the two operands. - * @throws PropertyException If either operand is null. - */ - private Property evalMultiply(Numeric op1, - Numeric op2) throws PropertyException { - if (op1 == null || op2 == null) - throw new PropertyException("Non numeric operand in multiplication"); - return new NumericProperty(op1.multiply(op2)); - } - - - /** - * Evaluate a division operation. If either of the arguments is null, - * this means that it wasn't convertible to a Numeric value. - * @param op1 A Numeric object (Number or Length-type object) - * @param op2 A Numeric object (Number or Length-type object) - * @return A new NumericProperty object holding an object which represents - * op1 divided by op2. - * @throws PropertyException If either operand is null. - */ - private Property evalDivide(Numeric op1, - Numeric op2) throws PropertyException { - if (op1 == null || op2 == null) - throw new PropertyException("Non numeric operand in division"); - return new NumericProperty(op1.divide(op2)); - } - - /** - * Evaluate a modulo operation. If either of the arguments is null, - * this means that it wasn't convertible to a Number value. - * @param op1 A Number object - * @param op2 A Number object - * @return A new NumberProperty object holding an object which represents - * op1 mod op2. - * @throws PropertyException If either operand is null. - */ - private Property evalModulo(Number op1, - Number op2) throws PropertyException { - if (op1 == null || op2 == null) - throw new PropertyException("Non number operand to modulo"); - return new NumberProperty(op1.doubleValue() % op2.doubleValue()); - } - } diff --git a/src/org/apache/fop/fo/expr/PropertyTokenizer.java b/src/org/apache/fop/fo/expr/PropertyTokenizer.java index 025c68695..590b9b42c 100644 --- a/src/org/apache/fop/fo/expr/PropertyTokenizer.java +++ b/src/org/apache/fop/fo/expr/PropertyTokenizer.java @@ -7,7 +7,10 @@ package org.apache.fop.fo.expr; - +import org.apache.fop.datatypes.Numeric; +import org.apache.fop.datatypes.Length; +import org.apache.fop.datatypes.Frequency; +import org.apache.fop.datatypes.Time; /** * Class to tokenize XSL FO property expression. @@ -16,44 +19,92 @@ package org.apache.fop.fo.expr; */ class PropertyTokenizer { - static final int TOK_EOF = 0; - static final int TOK_NCNAME = TOK_EOF + 1; - static final int TOK_MULTIPLY = TOK_NCNAME + 1; - static final int TOK_LPAR = TOK_MULTIPLY + 1; - static final int TOK_RPAR = TOK_LPAR + 1; - static final int TOK_LITERAL = TOK_RPAR + 1; - static final int TOK_NUMBER = TOK_LITERAL + 1; - static final int TOK_FUNCTION_LPAR = TOK_NUMBER + 1; - static final int TOK_PLUS = TOK_FUNCTION_LPAR + 1; - static final int TOK_MINUS = TOK_PLUS + 1; - static final int TOK_MOD = TOK_MINUS + 1; - static final int TOK_DIV = TOK_MOD + 1; - static final int TOK_NUMERIC = TOK_DIV + 1; - static final int TOK_COMMA = TOK_NUMERIC + 1; - static final int TOK_PERCENT = TOK_COMMA + 1; - static final int TOK_COLORSPEC = TOK_PERCENT + 1; - static final int TOK_FLOAT = TOK_COLORSPEC + 1; - static final int TOK_INTEGER = TOK_FLOAT + 1; + /* + * Maintain the numbering of this list in (X)Emacs by issuing + * a shell command on the region with replacement (M-1 M-|). Use + * the perl command: + * perl -p -e 'BEGIN{$n=0};$n++ if s/= [0-9]+/= $n/' + * + * in vi, set mark `a' at the last line and + * !'aperl... etc + */ + static final int + EOF = 0 + ,NCNAME = 1 + ,MULTIPLY = 2 + ,LPAR = 3 + ,RPAR = 4 + ,LITERAL = 5 + ,FUNCTION_LPAR = 6 + ,PLUS = 7 + ,MINUS = 8 + ,MOD = 9 + ,DIV = 10 + ,COMMA = 11 + ,PERCENT = 12 + ,COLORSPEC = 13 + ,FLOAT = 14 + ,INTEGER = 15 + ,ABSOLUTE_LENGTH = 16 + ,RELATIVE_LENGTH = 17 + ,TIME = 18 + ,FREQ = 19 + ,ANGLE = 20 + ,INHERIT = 21 + ,AUTO = 22 + ,NONE = 23 + ,BOOL = 24 + ,URI = 25 + ,MIMETYPE = 26 + // NO_UNIT is a transient token for internal use only. It is + // never set as the end result of parsing a token. + ,NO_UNIT = 27 + //,NSPREFIX = 28 + //,WHITESPACE = 29 + ; - int currentToken = TOK_EOF; + /* + * Absolute unit type constants + */ + int currentToken = EOF; String currentTokenValue = null; protected int currentUnitLength = 0; + protected int currentUnit; + protected String uri; private int currentTokenStartIndex = 0; - private /* final */ String expr; + private String expr = null; private int exprIndex = 0; private int exprLength; - private boolean recognizeOperator = false; + protected int property; + protected PropertyTokenizer() {} /** - * Construct a new PropertyTokenizer object to tokenize the passed - * String. - * @param s The Property expressio to tokenize. + * Initialize this tokenizer to tokenize the passed + * String as a value of the passed property. + * It is assumed that the subclass has made any necessary + * synchronization arrangements. + * @param property an int containing the property index. + * @param s The Property expression to tokenize. */ - PropertyTokenizer(String s) { - this.expr = s; - this.exprLength = s.length(); + protected void initialize(int property, String s) { + expr = s; + exprLength = s.length(); + this.property = property; + } + + /** + * Reset the tokenizer to null (or equivalent) values. + * Synchronization is achieved in the subclass. + */ + protected void reset() { + expr = null; + exprIndex = 0; + exprLength = 0; + currentToken = EOF; + currentTokenValue = null; + property = 0; } /** @@ -61,19 +112,17 @@ class PropertyTokenizer { * This sets the following package visible variables: * currentToken An enumerated value identifying the recognized token * currentTokenValue A String containing the token contents - * currentUnitLength If currentToken = TOK_NUMERIC, the number of - * characters in the unit name. + * currentUnit If currentToken = ABSOLUTE_LENGTH, TIME or FREQUENCY, + * an enumerated value identifying the unit. * @throws PropertyException If un unrecognized token is encountered. */ void next() throws PropertyException { currentTokenValue = null; currentTokenStartIndex = exprIndex; - boolean currentMaybeOperator = recognizeOperator; boolean bSawDecimal; - recognizeOperator = true; for (; ; ) { if (exprIndex >= exprLength) { - currentToken = TOK_EOF; + currentToken = EOF; return; } char c = expr.charAt(exprIndex++); @@ -82,26 +131,29 @@ class PropertyTokenizer { case '\t': case '\r': case '\n': - currentTokenStartIndex = exprIndex; + // Whitespace characters are valid within strings. + // in font family names, sequences of whitespace are + // compressed into a single space. (Rec 7.8.2) + //scanWhitespace(); + //currentToken = WHITESPACE; + //currentTokenValue = expr.substring(currentTokenStartIndex, + // exprIndex); + //return; break; case ',': - recognizeOperator = false; - currentToken = TOK_COMMA; + currentToken = COMMA; return; case '+': - recognizeOperator = false; - currentToken = TOK_PLUS; + currentToken = PLUS; return; case '-': - recognizeOperator = false; - currentToken = TOK_MINUS; + currentToken = MINUS; return; case '(': - currentToken = TOK_LPAR; - recognizeOperator = false; + currentToken = LPAR; return; case ')': - currentToken = TOK_RPAR; + currentToken = RPAR; return; case '"': case '\'': @@ -112,19 +164,10 @@ class PropertyTokenizer { } currentTokenValue = expr.substring(currentTokenStartIndex + 1, exprIndex++); - currentToken = TOK_LITERAL; + currentToken = LITERAL; return; case '*': - /* - * if (currentMaybeOperator) { - * recognizeOperator = false; - */ - currentToken = TOK_MULTIPLY; - /* - * } - * else - * throw new PropertyException("illegal operator *"); - */ + currentToken = MULTIPLY; return; case '0': case '1': @@ -148,15 +191,16 @@ class PropertyTokenizer { } else bSawDecimal = false; if (exprIndex < exprLength && expr.charAt(exprIndex) == '%') { + currentToken = PERCENT; + currentTokenValue = expr.substring(currentTokenStartIndex, + exprIndex); exprIndex++; - currentToken = TOK_PERCENT; + return; } else { // Check for possible unit name following number - currentUnitLength = exprIndex; - scanName(); - currentUnitLength = exprIndex - currentUnitLength; - currentToken = (currentUnitLength > 0) ? TOK_NUMERIC - : (bSawDecimal ? TOK_FLOAT : TOK_INTEGER); + currentToken = scanUnitName(); + if (currentToken == NO_UNIT) + currentToken = bSawDecimal ? FLOAT : INTEGER; } currentTokenValue = expr.substring(currentTokenStartIndex, exprIndex); @@ -170,14 +214,12 @@ class PropertyTokenizer { if (exprIndex < exprLength && expr.charAt(exprIndex) == '%') { exprIndex++; - currentToken = TOK_PERCENT; + currentToken = PERCENT; } else { // Check for possible unit name following number - currentUnitLength = exprIndex; - scanName(); - currentUnitLength = exprIndex - currentUnitLength; - currentToken = (currentUnitLength > 0) ? TOK_NUMERIC - : TOK_FLOAT; + currentToken = scanUnitName(); + if (currentToken == NO_UNIT) + currentToken = FLOAT; } currentTokenValue = expr.substring(currentTokenStartIndex, exprIndex); @@ -188,50 +230,165 @@ class PropertyTokenizer { case '#': // Start of color value if (exprIndex < exprLength && isHexDigit(expr.charAt(exprIndex))) { + int len; ++exprIndex; scanHexDigits(); - currentToken = TOK_COLORSPEC; + currentToken = COLORSPEC; currentTokenValue = expr.substring(currentTokenStartIndex, exprIndex); // Probably should have some multiple of 3 for length! - return; - } else + len = exprIndex - currentTokenStartIndex; + if (len == 4 || len == 7) return; + throw new PropertyException("color not 3 or 6 hex digits"); + } else { throw new PropertyException("illegal character '#'"); + } default: --exprIndex; scanName(); if (exprIndex == currentTokenStartIndex) - throw new PropertyException("illegal character"); + // Not a name - must be a + throw new PropertyException + ("illegal character '" + + expr.charAt(exprIndex) + "'"); currentTokenValue = expr.substring(currentTokenStartIndex, - exprIndex); - // if (currentMaybeOperator) { + exprIndex); if (currentTokenValue.equals("mod")) { - currentToken = TOK_MOD; + currentToken = MOD; return; - } else if (currentTokenValue.equals("div")) { - currentToken = TOK_DIV; + } + if (currentTokenValue.equals("div")) { + currentToken = DIV; return; } - /* - * else - * throw new PropertyException("unrecognized operator name"); - * recognizeOperator = false; - * return; - * } - */ + if (currentTokenValue.equals("inherit")) { + currentToken = INHERIT; + return; + } + if (currentTokenValue.equals("auto")) { + currentToken = AUTO; + return; + } + if (currentTokenValue.equals("none")) { + currentToken = NONE; + return; + } + if (currentTokenValue.equals("true") + || currentTokenValue.equals("false")) { + currentToken = BOOL; + return; + } + // Quick and dirty url "parsing". Assume that a + // URI-SPECIFICATION must be the only component of a + // property value expression + if (currentTokenValue.equals("url") + && expr.charAt(exprIndex) == '(') { + if (! scanUrl()) { + throw new PropertyException + ("Invalid url expression :" + + expr.substring(exprIndex)); + } + currentToken = URI; + return; + } + if (currentTokenValue.equals("content-type")) { + // content-type attribute value. Must be followed + // by a mime type + if (expr.charAt(exprIndex) == ':') { + int mimeptr = ++exprIndex; + scanMimeType(); + currentToken = MIMETYPE; + currentTokenValue = + expr.substring(mimeptr, exprIndex); + return; + } + // else it's just a name + } + if (currentTokenValue.equals("namespace-prefix")) { + // content-type attribute value. Must be followed + // by a declared namespace-prefix or null + if (expr.charAt(exprIndex) == ':') { + int nsptr = ++exprIndex; + scanName(); // Allowed to be empty + currentToken = NCNAME; + currentTokenValue = + expr.substring(nsptr, exprIndex); + return; + } + // else it's just a name + } if (followingParen()) { - currentToken = TOK_FUNCTION_LPAR; - recognizeOperator = false; + currentToken = FUNCTION_LPAR; } else { - currentToken = TOK_NCNAME; - recognizeOperator = false; + currentToken = NCNAME; } return; } } } + /** + * Attempt to recognize a valid UnitName token in the input expression. + * @return token value appropriate to UnitName: ABSOLUTE_LENGTH, + * RELATIVE_LENGTH or NO_UNIT. + * @exception PropertyException if an NCName not a UnitName recognized. + */ + private int scanUnitName() throws PropertyException { + String unit; + currentUnitLength = exprIndex; + scanName(); + if ((currentUnitLength = exprIndex - currentUnitLength) > 0) { + unit = expr.substring(currentUnitLength, exprIndex); + if (unit.equals("em")) return RELATIVE_LENGTH; + if (unit.equals("cm")) { + currentUnit = Length.CM; + return ABSOLUTE_LENGTH; + } + if (unit.equals("mm")) { + currentUnit = Length.MM; + return ABSOLUTE_LENGTH; + } + if (unit.equals("in")) { + currentUnit = Length.IN; + return ABSOLUTE_LENGTH; + } + if (unit.equals("pt")) { + currentUnit = Length.PT; + return ABSOLUTE_LENGTH; + } + if (unit.equals("pc")) { + currentUnit = Length.PC; + return ABSOLUTE_LENGTH; + } + if (unit.equals("px")) { + currentUnit = Length.PX; + return ABSOLUTE_LENGTH; + } + if (unit.equals("s")) { + currentUnit = Time.SEC; + return TIME; + } + if (unit.equals("ms")) { + currentUnit = Time.MSEC; + return TIME; + } + if (unit.equals("Hz")) { + currentUnit = Frequency.HZ; + return FREQ; + } + if (unit.equals("kHz")) { + currentUnit = Frequency.KHZ; + return FREQ; + } + // Not a UnitName + throw new PropertyException + ("NCName following a number is not a UnitName"); + } else { // No NCName found + return NO_UNIT; + } + } + /** * Attempt to recognize a valid NAME token in the input expression. */ @@ -250,6 +407,15 @@ class PropertyTokenizer { exprIndex++; } + /** + * Scan to the end of a sequence of whitespace characters in the + * input expression. + */ + private void scanWhitespace() { + while (exprIndex < exprLength && isSpace(expr.charAt(exprIndex))) + exprIndex++; + } + /** * Attempt to recognize a valid sequence of hexadecimal digits in the * input expression. @@ -260,7 +426,26 @@ class PropertyTokenizer { } /** - * Return a boolean value indicating whether the following non-whitespace + * Attempt to recognize a mime-type. Working definition here: + * NCName/NCName (NCName as recognized by scanName()). + */ + private void scanMimeType() throws PropertyException { + int part1 = exprIndex; + scanName(); + if (part1 != exprIndex) { + if (expr.charAt(exprIndex) == '/') { + int part2 = ++exprIndex; + scanName(); + if (part2 != exprIndex) + return; + } + } + throw new PropertyException("Mime type expected; found:" + + expr.substring(part1)); + } + + /** + * @return a boolean value indicating whether the following non-whitespace * character is an opening parenthesis. */ private boolean followingParen() { @@ -281,6 +466,30 @@ class PropertyTokenizer { return false; } + /** + * Primitive URI extractor. Assumes that the only contents of a + * URI-SPECIFICATION property type is a complete uri-specification. + * No checking is done on the syntactical validity of the URI. + * @return a boolean indicating whether the remainder of the + * characters form the body of a url(...) specification. + * As a side-effect, sets the protected field uri + * and sets exprIndex past the end of the expression, when + * returning a true value. + */ + private boolean scanUrl() { + char ch; + String str = expr.substring(exprIndex).trim(); + if (str.charAt(str.length() - 1) != ')') return false; + // Remove closing parenthesis and trim + str = str.substring(0, str.length() - 1).trim(); + if ((ch = str.charAt(0)) == '"' || ch == '\'') { + if (str.charAt(str.length() - 1) != ch) return false; + str = str.substring(1, str.length() - 1); + } + uri = str.trim(); + exprIndex = expr.length(); + return true; + } static private final String nameStartChars = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; -- 2.39.5