1   package org.daisy.pipeline.css.impl;
2   
3   import java.util.AbstractCollection;
4   import java.util.ArrayList;
5   import java.util.HashMap;
6   import java.util.Iterator;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Optional;
10  import java.util.regex.Matcher;
11  import java.util.regex.Pattern;
12  
13  import com.google.common.collect.Iterables;
14  
15  import org.daisy.pipeline.css.sass.impl.SassMapParser;
16  
17  import org.slf4j.Logger;
18  import org.slf4j.LoggerFactory;
19  
20  import org.unbescape.css.CssEscape;
21  
22  public abstract class StylesheetParametersParser {
23  
24  	private static final Logger logger = LoggerFactory.getLogger(StylesheetParametersParser.class);
25  	private static final Pattern regex = Pattern.compile(
26  		(SassMapParser.asRegex().pattern() + "|" + Query.QUERY.pattern()).replaceAll("\\(\\?<[^>]+>", "(?:"));
27  
28  	private StylesheetParametersParser() {}
29  
30  	public static Map<String,Object> parse(String expression) throws IllegalArgumentException {
31  		Map<String,Object> map = null;
32  		try {
33  			map = SassMapParser.parse(expression);
34  		} catch (IllegalArgumentException e) {
35  			Query query = null; {
36  				try {
37  					query = Query.parse(expression);
38  					logger.warn(
39  						"Query syntax is deprecated for stylesheet-parameters. Please use Sass map syntax.");
40  				} catch (IllegalArgumentException ee) {
41  				}
42  			}
43  			if (query != null) {
44  				map = new HashMap<>();
45  				for (Query.Feature f : query)
46  					map.put(f.getKey(), f.getValue().orElse("true"));
47  			}
48  		}
49  		if (map == null)
50  			throw new IllegalArgumentException("Could not parse stylesheet parameter set: " + expression);
51  		return map;
52  	}
53  
54  	public static Pattern asRegex() {
55  		return regex;
56  	}
57  
58  	/*
59  	 * The code below is copied from the Query class in braille-common. The class is not referenced
60  	 * because css-utils is a dependency of braille-common. For now we chose to copy rather than to
61  	 * move the class because the query syntax was introduced as a way to select braille
62  	 * transformers. Later we started using it to parse "stylesheet-parameter" options also, but
63  	 * only because it was convenient. Now we have a different syntax for stylesheet parameters,
64  	 * namely the Sass map syntax. For backward compatibility the query syntax is also still supported.
65  	 */
66  	private static class Query extends AbstractCollection<Query.Feature> {
67  
68  		private static final String IDENT_RE = "[_a-zA-Z][_a-zA-Z0-9-]*";
69  		private static final String STRING_RE = "'[^']*'|\"[^\"]*\"";
70  		private static final String INTEGER_RE = "0|-?[1-9][0-9]*";
71  		private static final Pattern VALUE_RE = Pattern.compile(
72  			"(?<ident>" + IDENT_RE + ")|(?<string>" + STRING_RE + ")|(?<integer>" + INTEGER_RE + ")"
73  		);
74  		private static final Pattern FEATURE_RE = Pattern.compile(
75  			"\\(\\s*(?<key>" + IDENT_RE + ")(?:\\s*\\:\\s*(?<value>" + VALUE_RE.pattern() + "))?\\s*\\)"
76  		);
77  		private static final Pattern FEATURES_RE = Pattern.compile(
78  			"\\s*(?:" + FEATURE_RE.pattern() + "\\s*)*"
79  		);
80  		private static final Pattern QUERY = FEATURES_RE;
81  
82  		/**
83  		 * Parse string to query
84  		 */
85  		public static Query parse(String query) throws IllegalArgumentException {
86  			if (FEATURES_RE.matcher(query).matches()) {
87  				Query q = new Query();
88  				Matcher m = FEATURE_RE.matcher(query);
89  				while (m.find()) {
90  					String key = m.group("key");
91  					String value = m.group("value");
92  					boolean isString = false;
93  					if (value != null) {
94  						Matcher m2 = VALUE_RE.matcher(value);
95  						if (!m2.matches())
96  							throw new RuntimeException("Coding error");
97  						String ident = m2.group("ident");
98  						String string = m2.group("string");
99  						String integer = m2.group("integer");
100 						if (ident != null)
101 							value = ident;
102 						else if (string != null && !string.equals("")) {
103 							value = CssEscape.unescapeCss(string.substring(1, string.length() - 1));
104 							isString = true; }
105 						else if (integer != null && !integer.equals(""))
106 							value = integer;
107 						else
108 							throw new RuntimeException("Coding error"); }
109 					q.add(new Feature(key, Optional.ofNullable(value), isString)); }
110 				return q; }
111 			throw new IllegalArgumentException("Could not parse query: " + query);
112 		}
113 
114 		private final List<Feature> list;
115 
116 		private Query() {
117 			this.list = new ArrayList<Feature>();
118 		}
119 
120 		@Override
121 		public boolean add(Feature feature) {
122 			int index = 0;
123 			String key = feature.getKey();
124 			for (Feature f : list) {
125 				int cmp = f.getKey().compareTo(key);
126 				if (cmp > 0) {
127 					break;
128 				} else if (cmp > 0) {
129 					index++;
130 					break;
131 				}
132 				index++;
133 			}
134 			list.add(index, feature);
135 			return true;
136 		}
137 
138 		@Override
139 		public int size() {
140 			return list.size();
141 		}
142 
143 		@Override
144 		public Iterator<Feature> iterator() {
145 			return list.iterator();
146 		}
147 
148 		@Override
149 		public String toString() {
150 			StringBuilder b = new StringBuilder();
151 			for (Feature f : this)
152 				b.append(f);
153 			return b.toString();
154 		}
155 
156 		@Override
157 		public int hashCode() {
158 			return list.hashCode();
159 		}
160 
161 		@Override
162 		public boolean equals(Object obj) {
163 			if (this == obj)
164 				return true;
165 			if (obj == null)
166 				return false;
167 			if (!(obj instanceof Query))
168 				return false;
169 			return Iterables.elementsEqual(this, (Query)obj);
170 		}
171 
172 		private static class Feature {
173 
174 			private final String key;
175 			private final Optional<String> value;
176 			private final Optional<String> literal;
177 
178 			private Feature(String key, Optional<String> value) {
179 				this(key, value, false);
180 			}
181 
182 			private Feature(String key, Optional<String> value, boolean specifiedAsString) {
183 				this.key = key;
184 				this.value = value;
185 				if (value.isPresent()) {
186 					String v = value.get();
187 					if (!specifiedAsString && (v.matches(IDENT_RE) || v.matches(INTEGER_RE)))
188 						this.literal = Optional.of(v);
189 					else
190 						this.literal = Optional.of("\"" + v.replace("\n", "\\A ").replace("\"","\\22 ") + "\"");
191 				} else
192 					this.literal = Optional.empty();
193 			}
194 
195 			public String getKey() {
196 				return key;
197 			}
198 
199 			public boolean hasValue() {
200 				return getValue().isPresent();
201 			}
202 
203 			public Optional<String> getValue() {
204 				return value;
205 			}
206 
207 			public Optional<String> getLiteral() {
208 				return literal;
209 			}
210 
211 			@Override
212 			public String toString() {
213 				StringBuilder b = new StringBuilder();
214 				String k = getKey();
215 				if (!k.matches(IDENT_RE))
216 					throw new RuntimeException();
217 				b.append("(" + k);
218 				if (hasValue()) {
219 					b.append(":");
220 					b.append(getLiteral().get());
221 				}
222 				b.append(")");
223 				return b.toString();
224 			}
225 
226 			@Override
227 			public int hashCode() {
228 				final int prime = 31;
229 				int result = 1;
230 				result = prime * result + ((key == null) ? 0 : key.hashCode());
231 				result = prime * result + ((value == null) ? 0 : value.hashCode());
232 				return result;
233 			}
234 
235 			@Override
236 			public boolean equals(Object obj) {
237 				if (this == obj)
238 					return true;
239 				if (obj == null)
240 					return false;
241 				if (getClass() != obj.getClass())
242 					return false;
243 				Feature other = (Feature)obj;
244 				if (key == null) {
245 					if (other.getKey() != null)
246 						return false;
247 				} else if (!key.equals(other.getKey()))
248 					return false;
249 				if (value == null) {
250 					if (other.getValue() != null)
251 						return false;
252 				} else if (!value.equals(other.getValue()))
253 					return false;
254 				return true;
255 			}
256 		}
257 	}
258 }