// License: GPL. For details, see LICENSE file. options { STATIC = false; OUTPUT_DIRECTORY = "parsergen"; } PARSER_BEGIN(MapCSSParser) package org.openstreetmap.josm.gui.mappaint.mapcss.parsergen; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.openstreetmap.josm.gui.mappaint.Keyword; import org.openstreetmap.josm.gui.mappaint.mapcss.Condition; import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Context; import org.openstreetmap.josm.gui.mappaint.mapcss.Expression; import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule.Declaration; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; import org.openstreetmap.josm.gui.mappaint.mapcss.Selector; import org.openstreetmap.josm.gui.mappaint.mapcss.ExpressionFactory; import org.openstreetmap.josm.gui.mappaint.mapcss.LiteralExpression; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException; import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector; import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector; import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.LinkSelector; import org.openstreetmap.josm.tools.ColorHelper; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.Main; /** * MapCSS parser. * * Contains two independent grammars: * (a) the preprocessor and (b) the main mapcss parser. * * The preprocessor handles @media syntax. Basically this allows * to write one style for different versions of JOSM (or different editors). * When the @media condition is not fulfilled, it should simply skip over * the whole section and not attempt to parse the possibly unknown * grammar. It preserves whitespace and comments, in order to keep the * line and column numbers in the error messages correct for the second pass. * */ public class MapCSSParser { MapCSSStyleSource sheet; StringBuilder sb; int declarationCounter; /** * Nicer way to refer to a lexical state. */ public static enum LexicalState { PREPROCESSOR(0), /* the preprocessor */ DEFAULT(2); /* the main parser */ int idx; // the integer, which javacc assigns to this state LexicalState(int idx) { if (!this.name().equals(MapCSSParserTokenManager.lexStateNames[idx])) { throw new RuntimeException(); } this.idx = idx; } }; /** * Constructor which initializes the parser with a certain lexical state. */ public MapCSSParser(InputStream in, String encoding, LexicalState initState) { this(createTokenManager(in, encoding, initState)); declarationCounter = 0; } protected static MapCSSParserTokenManager createTokenManager(InputStream in, String encoding, LexicalState initState) { SimpleCharStream scs; try { scs = new SimpleCharStream(in, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } return new MapCSSParserTokenManager(scs, initState.idx); } } PARSER_END(MapCSSParser) /************* * Token definitions * * Lexical states for the preprocessor: , * Lexical states for the main parser: , */ TOKEN: { < PP_AND: "and" > | < PP_NOT: "not" > | < PP_MEDIA: "@media" > | < PP_NEWLINECHAR: "\n" | "\r" | "\f" > | < PP_WHITESPACE: " " | "\t" > | < PP_COMMENT_START: "/*" > : PP_COMMENT } TOKEN: { < PP_COMMENT_END: "*/" > : PREPROCESSOR } MORE: { < ~[] > } TOKEN [IGNORE_CASE]: { /* Special keyword in some contexts, ordinary identifier in other contexts. Use the parsing rule ident() to refer to a general identifier, including "set". */ < SET: "set" > } TOKEN: { < IDENT: ["a"-"z","A"-"Z","_"] ( ["a"-"z","A"-"Z","_","-","0"-"9"] )* > | < UINT: ["1"-"9"] ( ["0"-"9"] )* > | < STRING: "\"" ( [" ","!","#"-"[","]"-"~","\u0080"-"\uFFFF"] | "\\\"" | "\\\\" )* "\"" > | < #PREDEFINED: "\\" ["d","D","s","S","w","W"] > | < #REGEX_CHAR_WITHOUT_STAR: [" "-")","+"-".","0"-"[","]"-"~","\u0080"-"\uFFFF"] | "\\/" | "\\\\" | "\\[" | "\\]" | "\\+" | "\\." | "\\'" | "\\\"" | "\\(" | "\\)" | > | < REGEX: "/" ( | "*" )* "/" > | < LBRACE: "{" > | < RBRACE: "}" > | < LPAR: "(" > | < RPAR: ")" > | < COLON: ":" > } TOKEN: { < PP_SOMETHING_ELSE : ~[] > } TOKEN: { < UFLOAT: ( ["0"-"9"] )+ ( "." ( ["0"-"9"] )+ )? > | < #H: ["0"-"9","a"-"f","A"-"F"] > | < HEXCOLOR: "#" ( | | ) > | < S: ( " " | "\t" | "\n" | "\r" | "\f" )+ > | < STAR: "*" > | < SLASH: "/" > | < LSQUARE: "[" > | < RSQUARE: "]" > | < GREATER_EQUAL: ">=" > | < LESS_EQUAL: "<=" > | < GREATER: ">" > | < LESS: "<" > | < EQUAL: "=" > | < EXCLAMATION: "!" > | < TILDE: "~" > | < DCOLON: "::" > | < SEMICOLON: ";" > | < COMMA: "," > | < PIPE: "|" > | < PIPE_Z: "|z" > | < PLUS: "+" > | < MINUS: "-" > | < AMPERSAND: "&" > | < QUESTION: "?" > | < DOLLAR: "$" > | < CARET: "^" > | < FULLSTOP: "." > | < ELEMENT_OF: "∈" > | < CROSSING: "⧉" > | < COMMENT_START: "/*" > : COMMENT | < UNEXPECTED_CHAR : ~[] > // avoid TokenMgrErrors because they are hard to recover from } TOKEN: { < COMMENT_END: "*/" > : DEFAULT } SKIP: { < ~[] > } /************* * * Preprocessor parser definitions: * *
 *
 * {@literal @media} { ... } queries are supported, following http://www.w3.org/TR/css3-mediaqueries/#syntax
 *
 *                               media_query
 *         ___________________________|_______________________________
 *        |                                                           |
 * {@literal @media} all and (min-josm-version: 7789) and (max-josm-version: 7790), all and (user-agent: xyz) { ... }
 *                |______________________|
 *                          |
 *                    media_expression
 * 
*/ /** * root method for the preprocessor. */ String pp_root(MapCSSStyleSource sheet): { } { { sb = new StringBuilder(); this.sheet = sheet; } pp_black_box(true) { return sb.toString(); } } /** * Parse any unknown grammar (black box). * * Only stop when "@media" is encountered and keep track of correct number of * opening and closing curly brackets. * * @param write false if this content should be skipped (@pp_media condition is not fulfilled), true otherwise */ void pp_black_box(boolean write): { Token t; } { ( (t= | t= | t= | t= | t= | t= | t= | t= | t= | t=) { if (write) sb.append(t.image); } | pp_w1() | pp_media() | t= { if (write) sb.append(t.image); } pp_black_box(write) t= { if (write) sb.append(t.image); } )* } void pp_media(): { boolean pass = false; boolean q; boolean empty = true; } { pp_w() ( q=pp_media_query() { pass = pass || q; empty = false; } ( pp_w() q=pp_media_query() { pass = pass || q; } )* )? pp_black_box(empty || pass) } boolean pp_media_query(): { Token t; String mediatype = "all"; boolean pass = true; boolean invert = false; boolean e; } { ( { invert = true; } pp_w() )? ( t= { mediatype = t.image.toLowerCase(); } pp_w() ( pp_w() e=pp_media_expression() { pass = pass && e; } pp_w() )* | e=pp_media_expression() { pass = pass && e; } pp_w() ( pp_w() e=pp_media_expression() { pass = pass && e; } pp_w() )* ) { if (!"all".equals(mediatype)) { pass = false; } return invert ? (!pass) : pass; } } /** * Parse an @media expression. * * The parsing rule {@link #literal()} from the main mapcss parser is reused here. * * @return true if the condition is fulfilled */ boolean pp_media_expression(): { Token t; String feature; Object val = null; } { pp_w() t= { feature = t.image; } pp_w() ( pp_w() val=literal() )? { return this.sheet.evalMediaExpression(feature, val); } } void pp_w1(): { Token t; } { t= { sb.append(t.image); } | t= { sb.append(t.image); } | t= { sb.append(t.image); } t= { sb.append(t.image); } } void pp_w(): { } { ( pp_w1() )* } /************* * * Parser definition for the main MapCSS parser: * *
 *
 *                       rule
 *  _______________________|______________________________
 * |                                                      |
 *        selector                      declaration
 *  _________|___________________   _________|____________
 * |                             | |                      |
 *
 * way|z11-12[highway=residential] { color: red; width: 3 }
 *
 *    |_____||___________________|   |_________|
 *       |            |                   |
 *     zoom       condition          instruction
 *
 * more general:
 *
 * way|z13-[a=b][c=d]::subpart, way|z-3[u=v]:closed::subpart2 { p1 : val; p2 : val; }
 *
 * 'val' can be a literal, or an expression like "prop(width, default) + 0.8".
 *
 * 
*/ int uint() : { Token i; } { i= { return Integer.parseInt(i.image); } } int int_() : { int i; } { i=uint() { return -i; } | i=uint() { return i; } } float ufloat() : { Token f; } { ( f= | f= ) { return Float.parseFloat(f.image); } } float float_() : { float f; } { f=ufloat() { return -f; } | f=ufloat() { return f; } } String string() : { Token t; } { t= { return t.image.substring(1, t.image.length() - 1).replace("\\\"", "\"").replace("\\\\", "\\"); } } String ident(): { Token t; String s; } { ( t= | t= ) { return t.image; } } String string_or_ident() : { Token t; String s; } { ( s=ident() | s=string() ) { return s; } } String regex() : { Token t; } { t= { return t.image.substring(1, t.image.length() - 1); } } /** * white-space */ void s() : { } { ( )? } /** * mix of white-space and comments */ void w() : { } { ( | )* } /** * comma delimited list of floats (at least 2, all >= 0) */ List float_array() : { float f; List fs = new ArrayList(); } { f=ufloat() { fs.add(f); } ( s() f=ufloat() { fs.add(f); } )+ { return fs; } } /** * entry point for the main parser */ void sheet(MapCSSStyleSource sheet): { } { { this.sheet = sheet; } w() ( try { rule() w() } catch (MapCSSException mex) { error_skipto(RBRACE, mex); w(); } catch (ParseException ex) { error_skipto(RBRACE, null); w(); } )* } void rule(): { List selectors = new ArrayList(); Selector sel; Declaration decl; } { sel=child_selector() { selectors.add(sel); } ( w() sel=child_selector() { selectors.add(sel); } )* decl=declaration() { for (Selector s : selectors) { sheet.rules.add(new MapCSSRule(s, decl)); } } } Selector child_selector() : { Selector.ChildOrParentSelectorType type = null; Condition c; List conditions = new ArrayList(); Selector selLeft; LinkSelector selLink = null; Selector selRight = null; } { selLeft=selector() w() ( ( ( { type = Selector.ChildOrParentSelectorType.CHILD; } | { type = Selector.ChildOrParentSelectorType.PARENT; } | { type = Selector.ChildOrParentSelectorType.SIBLING; } ) ( ( c=condition(Context.LINK) | c=class_or_pseudoclass(Context.LINK) ) { conditions.add(c); } )* | { type = Selector.ChildOrParentSelectorType.ELEMENT_OF; } | { type = Selector.ChildOrParentSelectorType.CROSSING; } ) { selLink = new LinkSelector(conditions); } w() selRight=selector() w() )? { return selRight != null ? new ChildOrParentSelector(selLeft, selLink, selRight, type) : selLeft; } } Selector selector() : { Token base; Condition c; Pair r = null; List conditions = new ArrayList(); String sub = null; } { ( base= | base= ) ( r=zoom() )? ( ( c=condition(Context.PRIMITIVE) | c=class_or_pseudoclass(Context.PRIMITIVE) ) { conditions.add(c); } )* ( sub=subpart() )? { return new GeneralSelector(base.image, r, conditions, sub); } } Pair zoom() : { Integer min = 0; Integer max = Integer.MAX_VALUE; } { ( max=uint() | LOOKAHEAD(2) min=uint() ( max=uint() )? | min=uint() { max = min; } ) { return new Pair(min, max); } } Condition condition(Context context) : { Condition c; Expression e; } { s() ( LOOKAHEAD( simple_key_condition(context) s() ) c=simple_key_condition(context) s() { return c; } | LOOKAHEAD( simple_key_value_condition(context) s() ) c=simple_key_value_condition(context) s() { return c; } | e=expression() { return Condition.createExpressionCondition(e, context); } ) } String tag_key() : { String s, s2; Token t; } { s=string() { return s; } | s=ident() ( s2=ident() { s += ':' + s2; } )* { return s; } } Condition simple_key_condition(Context context) : { boolean not = false; Condition.KeyMatchType matchType = null;; String key; } { ( { not = true; } )? ( { matchType = Condition.KeyMatchType.REGEX; } key = regex() | key = tag_key() ) ( LOOKAHEAD(2) { matchType = Condition.KeyMatchType.FALSE; } )? ( { matchType = Condition.KeyMatchType.TRUE; } )? { return Condition.createKeyCondition(key, not, matchType, context); } } Condition simple_key_value_condition(Context context) : { String key; String val; float f; int i; Condition.Op op; boolean considerValAsKey = false; } { key=tag_key() s() ( LOOKAHEAD(3) ( { op=Condition.Op.REGEX; } | { op=Condition.Op.NREGEX; } ) s() ( { considerValAsKey=true; } )? val=regex() | ( { op=Condition.Op.NEQ; } | { op=Condition.Op.EQ; } | { op=Condition.Op.ONE_OF; } | { op=Condition.Op.BEGINS_WITH; } | { op=Condition.Op.ENDS_WITH; } | { op=Condition.Op.CONTAINS; } ) s() ( { considerValAsKey=true; } )? ( LOOKAHEAD(2) i=int_() { val=Integer.toString(i); } | f=float_() { val=Float.toString(f); } | val=string_or_ident() ) | ( { op=Condition.Op.GREATER_OR_EQUAL; } | { op=Condition.Op.GREATER; } | { op=Condition.Op.LESS_OR_EQUAL; } | { op=Condition.Op.LESS; } ) s() f=float_() { val=Float.toString(f); } ) { return Condition.createKeyValueCondition(key, val, op, context, considerValAsKey); } } Condition class_or_pseudoclass(Context context) : { String s; boolean not = false; boolean pseudo; } { ( { not = true; } )? ( { pseudo = false; } | { pseudo = true; } ) s=ident() { return pseudo ? Condition.createPseudoClassCondition(s, not, context) : Condition.createClassCondition(s, not, context); } } String subpart() : { String s; } { ( s=ident() { return s; } | { return "*"; } ) } Declaration declaration() : { List ins = new ArrayList(); Instruction i; Token key; Object val = null; } { w() ( ( w() ()? // specification allows "set .class" to set "class". we also support "set class" key= w() ( val=expression() )? { ins.add(new Instruction.AssignmentInstruction(key.image, val == null ? true : val, true)); } ( { return new Declaration(ins, declarationCounter++); } | w() ) ) | key= w() w() ( LOOKAHEAD( float_array() w() ( | ) ) val=float_array() { ins.add(new Instruction.AssignmentInstruction(key.image, val, false)); } w() ( { return new Declaration(ins, declarationCounter++); } | w() ) | LOOKAHEAD( expression() ( | ) ) val=expression() { ins.add(new Instruction.AssignmentInstruction(key.image, val, false)); } ( { return new Declaration(ins, declarationCounter++); } | w() ) | val=readRaw() w() { ins.add(new Instruction.AssignmentInstruction(key.image, val, false)); } ) )* { return new Declaration(ins, declarationCounter++); } } Expression expression(): { List args = new ArrayList(); Expression e; String op = null; } { ( { op = "not"; } w() e=primary() { args.add(e); } w() | { op = "minus"; } w() e=primary() { args.add(e); } w() | ( e=primary() { args.add(e); } w() ( ( { op = "plus"; } w() e=primary() { args.add(e); } w() )+ | ( { op = "times"; } w() e=primary() { args.add(e); } w() )+ | ( { op = "minus"; } w() e=primary() { args.add(e); } w() )+ | ( { op = "divided_by"; } w() e=primary() { args.add(e); } w() )+ | { op = "greater_equal"; } w() e=primary() { args.add(e); } w() | { op = "less_equal"; } w() e=primary() { args.add(e); } w() | { op = "greater"; } w() e=primary() { args.add(e); } w() | ( )? { op = "equal"; } w() e=primary() { args.add(e); } w() | { op = "less"; } w() e=primary() { args.add(e); } w() | { op = "and"; } w() e=primary() { args.add(e); } w() | { op = "or"; } w() e=primary() { args.add(e); } w() | { op = "cond"; } w() e=primary() { args.add(e); } w() w() e=primary() { args.add(e); } w() )? ) ) { if (op == null) return args.get(0); return ExpressionFactory.createFunctionExpression(op, args); } } Expression primary() : { Expression nested; Expression fn; Object lit; } { LOOKAHEAD(3) // both function and identifier start with an identifier (+ optional whitespace) fn=function() { return fn; } | lit=literal() { return new LiteralExpression(lit); } | w() nested=expression() { return nested; } } Expression function() : { Expression arg; String name; List args = new ArrayList(); } { name=ident() w() w() ( arg=expression() { args.add(arg); } ( w() arg=expression() { args.add(arg); } )* )? { return ExpressionFactory.createFunctionExpression(name, args); } } Object literal() : { String val, pref; Token t; float f; } { LOOKAHEAD(2) pref=ident() t= { return Main.pref.getColor("mappaint." + (sheet == null ? "MapCSS" : sheet.title) + "." + pref, ColorHelper.html2color(t.image)); } | t= { return new Keyword(t.image); } | val=string() { return val; } | f=ufloat() { return new Instruction.RelativeFloat(f); } | f=ufloat() { return f; } | t= { return ColorHelper.html2color(t.image); } } JAVACODE void error_skipto(int kind, MapCSSException me) { if (token.kind == EOF) throw new ParseException("Reached end of file while parsing"); Exception e = null; ParseException pe = generateParseException(); if (me != null) { me.setLine(pe.currentToken.next.beginLine); me.setColumn(pe.currentToken.next.beginColumn); e = me; } else { e = new ParseException(pe.getMessage()); // prevent memory leak } Main.error("Skipping to the next rule, because of an error:"); Main.error(e); if (sheet != null) { sheet.logError(e); } Token t; do { t = getNextToken(); } while (t.kind != kind && t.kind != EOF); if (t.kind == EOF) throw new ParseException("Reached end of file while parsing"); } JAVACODE /** * read everything to the next semicolon */ String readRaw() { Token t; StringBuilder s = new StringBuilder(); while (true) { t = getNextToken(); if ((t.kind == S || t.kind == STRING || t.kind == UNEXPECTED_CHAR) && t.image.contains("\n")) { ParseException e = new ParseException(String.format("Warning: end of line while reading an unquoted string at line %s column %s.", t.beginLine, t.beginColumn)); Main.error(e); if (sheet != null) { sheet.logError(e); } } if (t.kind == SEMICOLON || t.kind == EOF) break; s.append(t.image); } if (t.kind == EOF) throw new ParseException("Reached end of file while parsing"); return s.toString(); }