1   package org.daisy.pipeline.css;
2   
3   import java.util.Collection;
4   import java.util.HashMap;
5   import java.util.HashSet;
6   import java.util.LinkedList;
7   import java.util.Map;
8   import java.util.Set;
9   
10  import cz.vutbr.web.css.Term;
11  import cz.vutbr.web.css.TermFunction;
12  import cz.vutbr.web.css.TermIdent;
13  import cz.vutbr.web.css.TermPair;
14  import cz.vutbr.web.css.TermString;
15  
16  import org.daisy.braille.css.BrailleCSSProperty.Display;
17  
18  /**
19   * Traverses a document while keeping track of <a
20   * href="https://www.w3.org/TR/css-lists-3/#counter">counter</a> values and providing the ability to
21   * generate counter representations.
22   */
23  public abstract class CounterEvaluator<E> {
24  
25  	/**
26  	 * Get {@code counter-reset} property.
27  	 */
28  	protected abstract Collection<? extends TermPair<String,Integer>> getCounterReset(E elementStyle);
29  
30  	/**
31  	 * Get {@code counter-set} property.
32  	 */
33  	protected abstract Collection<? extends TermPair<String,Integer>> getCounterSet(E elementStyle);
34  
35  	/**
36  	 * Get {@code counter-increment} property.
37  	 */
38  	protected abstract Collection<? extends TermPair<String,Integer>> getCounterIncrement(E elementStyle);
39  
40  	/**
41  	 * Get {@code display} property.
42  	 */
43  	protected abstract Display getDisplay(E elementStyle);
44  
45  	/**
46  	 * Get {@code content} property of {@code ::marker} pseudo-element.
47  	 *
48  	 * {@code null} means there is no {@code ::marker} pseudo-element. An empty list means the
49  	 * pseudo-element has no content.
50  	 */
51  	protected abstract Collection<Term<?>> getMarkerContent(E elementStyle);
52  
53  	/**
54  	 * Get list counter style.
55  	 */
56  	protected abstract CounterStyle getListStyleType(E elementStyle, CounterStyle parentListStyleType);
57  
58  	/**
59  	 * Get counter style for given name
60  	 */
61  	protected abstract CounterStyle getNamedCounterStyle(String name);
62  
63  	private final LinkedList<CounterStyle> listCounterStyle = new LinkedList<>();
64  	protected final LinkedList<Map<String,Integer>> counterValues = new LinkedList<>();
65  	private boolean previousEventWasStartElement = true; // false = endElement
66  
67  	/**
68  	 * This method must be called at the start of every element while traversing the document tree
69  	 * in depth-first order.
70  	 *
71  	 * New counter values are determined based on the given style.
72  	 *
73  	 * @param elementStyle The style of the current element, possibly containing {@code
74  	 *                     counter-reset}, {@code counter-set} and {@code counter-increment}
75  	 *                     properties.
76  	 */
77  	public void startElement(E elementStyle) {
78  		if (listCounterStyle.isEmpty())
79  			listCounterStyle.push(CounterStyle.DISC);
80  		if (previousEventWasStartElement)
81  			counterValues.push(null);
82  		previousEventWasStartElement = true;
83  		updateCounterValues(elementStyle);
84  		updateCurrentListCounterStyle(elementStyle);
85  	}
86  
87  	/**
88  	 * This method must be called at the end of every element while traversing the document tree in
89  	 * depth-first order.
90  	 */
91  	public void endElement() {
92  		listCounterStyle.pop();
93  		if (!previousEventWasStartElement)
94  			counterValues.pop();
95  		previousEventWasStartElement = false;
96  	}
97  
98  	/**
99  	 * Determine current counter values (see https://www.w3.org/TR/css-lists-3/#creating-counters)
100 	 */
101 	private void updateCounterValues(E elementStyle) {
102 		// counter-reset
103 		{
104 			Collection<? extends TermPair<String,Integer>> list = getCounterReset(elementStyle);
105 			if (list != null) {
106 				for (TermPair<String,Integer> p : list) {
107 					String name = p.getKey();
108 					Map<String,Integer> createdBySiblingOrSelf = counterValues.peek();
109 					if (createdBySiblingOrSelf == null) {
110 						createdBySiblingOrSelf = new HashMap<>();
111 						counterValues.set(0, createdBySiblingOrSelf);
112 					}
113 					createdBySiblingOrSelf.put(name, p.getValue());
114 				}
115 			}
116 		}
117 		// counter-set or counter-increment work in addition to counter-reset
118 		Set<String> done = null;
119 		{
120 			Collection<? extends TermPair<String,Integer>> list = getCounterSet(elementStyle);
121 			if (list != null) {
122 				for (TermPair<String,Integer> p : list) {
123 					String name = p.getKey();
124 					boolean exists = false;
125 					for (Map<String,Integer> c : counterValues) {
126 						if (c != null) {
127 							c.put(name, p.getValue());
128 							exists = true;
129 							break;
130 						}
131 					}
132 					if (!exists) {
133 						Map<String,Integer> createdBySiblingOrSelf = counterValues.peek();
134 						if (createdBySiblingOrSelf == null) {
135 							createdBySiblingOrSelf = new HashMap<>();
136 							counterValues.set(0, createdBySiblingOrSelf);
137 						}
138 						createdBySiblingOrSelf.put(name, p.getValue());
139 					}
140 					if (done == null)
141 						done = new HashSet<>();
142 					done.add(name);
143 				}
144 			}
145 		}
146 		{
147 			Collection<? extends TermPair<String,Integer>> list = getCounterIncrement(elementStyle);
148 			if (list != null) {
149 				for (TermPair<String,Integer> p : list) {
150 					String name = p.getKey();
151 					if (done == null || !done.contains(name)) {
152 						boolean exists = false;
153 						for (Map<String,Integer> c : counterValues) {
154 							if (c != null) {
155 								c.put(name, c.getOrDefault(name, 0) + p.getValue());
156 								exists = true;
157 								break;
158 							}
159 						}
160 						if (!exists) {
161 							Map<String,Integer> createdBySiblingOrSelf = counterValues.peek();
162 							if (createdBySiblingOrSelf == null) {
163 								createdBySiblingOrSelf = new HashMap<>();
164 								counterValues.set(0, createdBySiblingOrSelf);
165 							}
166 							createdBySiblingOrSelf.put(name, p.getValue());
167 						}
168 						if (done == null)
169 							done = new HashSet<>();
170 						done.add(name);
171 					}
172 				}
173 			}
174 		}
175 		// by default list items increment the 'list-item' counter
176 		if ((done == null || !done.contains("list-item")) && getDisplay(elementStyle) == Display.LIST_ITEM) {
177 			boolean exists = false;
178 			for (Map<String,Integer> c : counterValues) {
179 				if (c != null) {
180 					c.put("list-item", c.getOrDefault("list-item", 0) + 1);
181 					exists = true;
182 					break;
183 				}
184 			}
185 			if (!exists) {
186 				Map<String,Integer> createdBySiblingOrSelf = counterValues.peek();
187 				if (createdBySiblingOrSelf == null) {
188 					createdBySiblingOrSelf = new HashMap<>();
189 					counterValues.set(0, createdBySiblingOrSelf);
190 				}
191 				createdBySiblingOrSelf.put("list-item", 1);
192 			}
193 		}
194 	}
195 
196 	/**
197 	 * Determine current list counter style
198 	 */
199 	private void updateCurrentListCounterStyle(E elementStyle) {
200 		listCounterStyle.push(getListStyleType(elementStyle, listCounterStyle.peek()));
201 	}
202 
203 	/**
204 	 * Evaluate marker contents (see https://www.w3.org/TR/css-lists-3/#content-property)
205 	 */
206 	public String generateMarkerContents(E elementStyle) throws IllegalArgumentException {
207 		if (getDisplay(elementStyle) == Display.LIST_ITEM) {
208 			Collection<Term<?>> content = getMarkerContent(elementStyle);
209 			if (content != null) {
210 				if (!content.isEmpty())
211 					return evaluateContent(content);
212 				else
213 					return null;
214 			}
215 			CounterStyle listCounterStyle = this.listCounterStyle.peek();
216 			if (listCounterStyle != null)
217 				return evaluateCounter("list-item", listCounterStyle, true);
218 		}
219 		return null;
220 	}
221 
222 	private String evaluateContent(Collection<Term<?>> content) throws IllegalArgumentException {
223 		StringBuilder s = new StringBuilder();
224 		for (Term<?> t : content) {
225 			if (t instanceof TermString) {
226 				s.append(((TermString)t).getValue());
227 			} else if (t instanceof TermFunction) {
228 				TermFunction f = (TermFunction)t;
229 				if ("counter".equalsIgnoreCase(f.getFunctionName())) {
230 					String name = null;
231 					CounterStyle style = null;
232 					for (Term<?> arg : f)
233 						if (name == null)
234 							if (arg instanceof TermIdent)
235 								name = ((TermIdent)arg).getValue();
236 							else
237 								throw new IllegalArgumentException(
238 									"invalid first argument of counter() function: should be the counter name");
239 						else if (style == null) {
240 							if (arg instanceof TermIdent)
241 								style = getNamedCounterStyle(((TermIdent)arg).getValue());
242 							else if (arg instanceof TermFunction)
243 								try {
244 									style = CounterStyle.fromSymbolsFunction((TermFunction)arg);
245 								} catch (IllegalArgumentException e) {
246 									throw new IllegalArgumentException(
247 										"invalid second argument of counter() function: should be a counter style", e);
248 								}
249 							else
250 								throw new IllegalArgumentException(
251 									"invalid second argument of counter() function: should be a counter style"); }
252 						else
253 							throw new IllegalArgumentException(
254 								"unexpected argument of counter() function: function takes at most two arguments");
255 					if (name == null)
256 						throw new IllegalArgumentException("counter() function requires at least one argument");
257 					if (style == null)
258 						style = CounterStyle.DECIMAL;
259 					s.append(evaluateCounter(name, style, false));
260 				} else if ("counters".equalsIgnoreCase(f.getFunctionName())) {
261 					String name = null;
262 					String separator = null;
263 					CounterStyle style = null;
264 					for (Term<?> arg : f)
265 						if (name == null)
266 							if (arg instanceof TermIdent)
267 								name = ((TermIdent)arg).getValue();
268 							else
269 								throw new IllegalArgumentException(
270 									"invalid first argument of counters() function: should be the counter name");
271 						else if (separator == null) {
272 							if (arg instanceof TermString)
273 								separator = ((TermString)arg).getValue();
274 							else
275 								throw new IllegalArgumentException(
276 									"invalid second argument of counters() function: should be a string"); }
277 						else if (style == null) {
278 							if (arg instanceof TermIdent)
279 								style = getNamedCounterStyle(((TermIdent)arg).getValue());
280 							else if (arg instanceof TermFunction)
281 								try {
282 									style = CounterStyle.fromSymbolsFunction((TermFunction)arg);
283 								} catch (IllegalArgumentException e) {
284 									throw new IllegalArgumentException(
285 										"invalid third argument of counters() function: should be a counter style", e);
286 								}
287 							else
288 								throw new IllegalArgumentException(
289 									"invalid third argument of counters() function: should be a counter style"); }
290 						else
291 							throw new IllegalArgumentException(
292 								"unexpected argument of counters() function: function takes at most three arguments");
293 					if (name == null || separator == null)
294 						throw new IllegalArgumentException("counters() function requires at least two arguments");
295 					if (style == null)
296 						style = CounterStyle.DECIMAL;
297 					s.append(evaluateCounters(name, style, separator));
298 				} else
299 					throw new RuntimeException(f.getFunctionName() + "() function not supported in content list"); // FIXME
300 			} else
301 				throw new IllegalStateException(); // cannot happen
302 		}
303 		return s.toString();
304 	}
305 
306 	/**
307 	 * Generate a counter representation of the innermost counter in the counter set with the given
308 	 * name (see https://www.w3.org/TR/css-lists-3/#counter-functions)
309 	 */
310 	public String evaluateCounter(String name, CounterStyle style, boolean withPrefixAndSuffix) {
311 		int value = evaluateCounter(name);
312 		return style.format(value, withPrefixAndSuffix);
313 	}
314 
315 	/**
316 	 * Get the value of the innermost counter in the counter set with the given name, or instaniate
317 	 * a counter with value {@code 0} (see https://www.w3.org/TR/css-lists-3/#counter-functions)
318 	 */
319 	public int evaluateCounter(String name) {
320 		Integer value = null;
321 		boolean exists = false;
322 		for (Map<String,Integer> c : counterValues)
323 			if (c != null && c.containsKey(name)) {
324 				value = c.get(name);
325 				break;
326 			}
327 		if (value == null) {
328 			value = 0;
329 			Map<String,Integer> createdBySiblingOrSelf = counterValues.peek();
330 			if (createdBySiblingOrSelf == null) {
331 				createdBySiblingOrSelf = new HashMap<>();
332 				counterValues.set(0, createdBySiblingOrSelf);
333 			}
334 			createdBySiblingOrSelf.put(name, value);
335 		}
336 		return value;
337 	}
338 
339 	/**
340 	 * Generate a counter representation of all the counters in the counter set with the given name
341 	 * (see https://www.w3.org/TR/css-lists-3/#counter-functions)
342 	 */
343 	public String evaluateCounters(String name, CounterStyle style, String separator) {
344 		StringBuilder s = new StringBuilder();
345 		for (Map<String,Integer> c : counterValues)
346 			if (c != null && c.containsKey(name)) {
347 				if (s.length() > 0)
348 					s.insert(0, separator);
349 				s.insert(0, style.format(c.get(name)));
350 			}
351 		if (s.length() == 0) {
352 			Map<String,Integer> createdBySiblingOrSelf = counterValues.peek();
353 			if (createdBySiblingOrSelf == null) {
354 				createdBySiblingOrSelf = new HashMap<>();
355 				counterValues.set(0, createdBySiblingOrSelf);
356 			}
357 			createdBySiblingOrSelf.put(name, 0);
358 			s.append(style.format(0));
359 		}
360 		return s.toString();
361 	}
362 }