1   package org.daisy.pipeline.css;
2   
3   import java.util.Arrays;
4   import java.util.Collections;
5   import java.util.HashMap;
6   import java.util.HashSet;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Set;
10  
11  import com.google.common.collect.Lists;
12  
13  import cz.vutbr.web.css.CSSFactory;
14  import cz.vutbr.web.css.MediaExpression;
15  import cz.vutbr.web.css.MediaSpec;
16  import cz.vutbr.web.css.MediaQuery;
17  import cz.vutbr.web.css.Term;
18  import cz.vutbr.web.css.TermIdent;
19  import cz.vutbr.web.css.TermInteger;
20  import cz.vutbr.web.csskit.antlr.CSSParserFactory;
21  import cz.vutbr.web.domassign.Analyzer;
22  
23  public class Medium {
24  
25  	public enum Type {
26  		EMBOSSED,
27  		BRAILLE,
28  		SPEECH,
29  		SCREEN,
30  		PRINT;
31  		public String toString() {
32  			return super.toString().toLowerCase();
33  		}
34  	}
35  
36  	private final Type type;
37  	private final Integer width;
38  	private final Integer height;
39  	private final Map<String,String> customFeatures;
40  
41  	// see https://drafts.csswg.org/mediaqueries/#media-descriptor-table
42  	private final static Set<String> knownFeatures = new HashSet<>(
43  		Arrays.asList(
44  			"any-hover", "any-pointer", "aspect-ratio", "color", "color-gamut", "color-index",
45  			"device-aspect-ratio", "device-height", "device-width", "grid", "height", "hover",
46  			"monochrome", "orientation", "overflow-block", "overflow-inline", "pointer", "resolution",
47  			"scan", "update", "width"));
48  
49  	/**
50  	 * @param customFeatures can be used to pass additional properties of the target medium. Must
51  	 *                       not include any <a
52  	 *                       href="https://drafts.csswg.org/mediaqueries/#media-descriptor-table">known
53  	 *                       features</a>
54  	 */
55  	private Medium(Type type, Integer width, Integer height, Map<String,String> customFeatures) {
56  		switch (type) {
57  		case EMBOSSED:
58  			this.type = type;
59  			this.width = width;
60  			this.height = height;
61  			break;
62  		case BRAILLE:
63  		case SPEECH:
64  		case SCREEN:
65  		case PRINT:
66  			if (width != null) {
67  				throw new IllegalArgumentException("Unexpected 'width' argument for medium '" + type + "'");
68  			}
69  			if (height != null) {
70  				throw new IllegalArgumentException("Unexpected 'height' argument for medium '" + type + "'");
71  			}
72  			this.type = type;
73  			this.width = this.height = null;
74  			break;
75  		default:
76  			throw new IllegalArgumentException("Unexpected medium: " + type);
77  		}
78  		this.customFeatures = customFeatures != null
79  			? Collections.unmodifiableMap(customFeatures)
80  			: Collections.emptyMap();
81  	}
82  
83  	/**
84  	 * The media type: "embossed", "braille", "speech", "screen" or "print".
85  	 */
86  	public Type getType() {
87  		return type;
88  	}
89  
90  	/**
91  	 * The width of the page, in braille cells, or null if undetermined or not applicable. Only
92  	 * applicable on type 'embossed'.
93  	 */
94  	public Integer getWidth() {
95  		return width;
96  	}
97  
98  	/**
99  	 * The height of the page, in braille cells, or null if undetermined or not applicable. Only
100 	 * applicable on type 'embossed'.
101 	 */
102 	public Integer getHeight() {
103 		return height;
104 	}
105 
106 	/**
107 	 * Additional properties of the target medium.
108 	 */
109 	public Map<String,String> getCustomFeatures() {
110 		return customFeatures;
111 	}
112 
113 	public boolean matches(String mediaQuery) {
114 		if (mediaQuery != null)
115 			return asMediaSpec().matches(CSSParserFactory.getInstance().parseMediaQuery(mediaQuery.trim()));
116 		else
117 			return asMediaSpec().matchesEmpty();
118 	}
119 
120 	public String toString() {
121 		StringBuilder s  = new StringBuilder();
122 		s.append(type.toString());
123 		if (width != null)
124 			s.append(" AND (width: ").append(width).append(")");
125 		if (height != null)
126 			s.append(" AND (height: ").append(height).append(")");
127 		for (String f : customFeatures.keySet())
128 			s.append(" AND (").append(f).append(": ").append(customFeatures.get(f)).append(")");
129 		return s.toString();
130 	}
131 
132 	public boolean equals(Object o) {
133 		if (!(o instanceof Medium))
134 			return false;
135 		Medium that = (Medium)o;
136 		return this.type == that.type
137 			&& this.width == that.width
138 			&& this.height == that.height;
139 	}
140 
141 	public int hashCode() {
142 		final int prime = 31;
143 		int result = 1;
144 		result = prime * result + type.hashCode();
145 		result = prime * result + width;
146 		result = prime * result + height;
147 		return result;
148 	}
149 
150 	public static Medium parse(String medium) throws IllegalArgumentException {
151 		List<MediaQuery> query = CSSParserFactory.getInstance().parseMediaQuery(medium);
152 		if (query.size() != 1)
153 			throw new IllegalArgumentException("Unexpected medium: " + medium);
154 		return fromMediaQuery(query.get(0));
155 	}
156 
157 	/**
158 	 * Parse comma-separated list of media
159 	 */
160 	public static List<Medium> parseMultiple(String media) throws IllegalArgumentException {
161 		return Lists.transform(
162 			CSSParserFactory.getInstance().parseMediaQuery(media),
163 			Medium::fromMediaQuery);
164 	}
165 
166 	private static Medium fromMediaQuery(MediaQuery query) throws IllegalArgumentException {
167 		Type type = null;
168 		try {
169 			type = Type.valueOf(query.getType().toUpperCase());
170 		} catch (IllegalArgumentException e) {
171 			throw new IllegalArgumentException("Unexpected medium type: " + query.getType());
172 		}
173 		if (query.isNegative())
174 			throw new IllegalArgumentException("Unexpected medium: contains NOT: " + query);
175 		Integer width = null;
176 		Integer height = null;
177 		Map<String,String> customFeatures = null;
178 		for (MediaExpression e : query) {
179 			String feature = e.getFeature();
180 			if ("width".equals(feature) || "height".equals(feature)) {
181 				if (e.size() != 1)
182 					throw new IllegalArgumentException("Unexpected value for medium feature: " + e);
183 				Term<?> v = e.get(0);
184 				if (!(v instanceof TermInteger))
185 					throw new IllegalArgumentException("Unexpected value for medium feature: " + e);
186 				Integer i = ((TermInteger)v).getIntValue();
187 				if ("width".equals(feature))
188 					width = i;
189 				else
190 					height = i;
191 			} else if (knownFeatures.contains(feature)) {
192 				throw new IllegalArgumentException("Unsupported medium feature: " + feature);
193 			} else {
194 				if (e.size() != 1)
195 					throw new IllegalArgumentException("Unexpected value for medium feature: " + e);
196 				Term<?> v = e.get(0);
197 				if (!(v instanceof TermIdent || v instanceof TermInteger))
198 					throw new IllegalArgumentException("Unexpected value for medium feature: " + e);
199 				if (customFeatures == null)
200 					customFeatures = new HashMap<>();
201 				customFeatures.put(feature, v.toString());
202 			}
203 		}
204 		return new Medium(type, width, height, customFeatures);
205 	}
206 
207 	private MediaSpec mediaSpec;
208 
209 	/**
210 	 * The medium as a jStyleParser {@link MediaSpec} object, used by {@link
211 	 * JStyleParserCssCascader} for passing to {@link CSSFactory#getUsedStyles} and {@link
212 	 * Analyzer#evaluateDOM}.
213 	 */
214 	public MediaSpec asMediaSpec() {
215 		if (mediaSpec == null)
216 			switch (type) {
217 			case EMBOSSED:
218 				mediaSpec = new MediaSpec(type.toString()) {
219 						// overriding getExpressionLengthPx() because MediaSpec expects width and height
220 						// to be in pixels but width and height are specified in braille cells
221 						@Override
222 						protected Float getExpressionLengthPx(MediaExpression e) {
223 							return (float)getExpressionInteger(e);
224 						}
225 						@Override
226 						public boolean matches(MediaExpression e) {
227 							String f = e.getFeature();
228 							if (knownFeatures.contains(f) || knownFeatures.contains(f.replaceAll("^(min|max)-", "")))
229 								return super.matches(e);
230 							else {
231 								if (e.size() != 1)
232 									return false;
233 								Term<?> v = e.get(0);
234 								if (!(v instanceof TermIdent || v instanceof TermInteger))
235 									return false;
236 								if (!customFeatures.isEmpty())
237 									for (String ff : customFeatures.keySet())
238 										if (ff.equals(f))
239 											return customFeatures.get(ff).equals(v.toString());
240 								return false;
241 							}
242 						}
243 					};
244 				if (width != null)
245 					mediaSpec.setWidth((float)width);
246 				if (height != null)
247 					mediaSpec.setWidth((float)height);
248 				break;
249 			default:
250 				mediaSpec = new MediaSpec(type.toString()) {
251 						@Override
252 						public boolean matches(MediaExpression e) {
253 							String f = e.getFeature();
254 							if (knownFeatures.contains(f) || knownFeatures.contains(f.replaceAll("^(min|max)-", "")))
255 								return super.matches(e);
256 							else {
257 								if (e.size() != 1)
258 									return false;
259 								Term<?> v = e.get(0);
260 								if (!(v instanceof TermIdent || v instanceof TermInteger))
261 									return false;
262 								if (!customFeatures.isEmpty())
263 									for (String ff : customFeatures.keySet())
264 										if (ff.equals(f))
265 											return customFeatures.get(ff).equals(v.toString());
266 								return false;
267 							}
268 						}
269 					};
270 				break;
271 			}
272 		return mediaSpec;
273 	}
274 }