1   package org.daisy.pipeline.css;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.function.Supplier;
6   import java.util.HashMap;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Optional;
11  
12  import com.google.common.collect.ImmutableList;
13  import com.google.common.collect.Iterables;
14  
15  import cz.vutbr.web.css.Declaration;
16  import cz.vutbr.web.css.Term;
17  import cz.vutbr.web.css.TermFunction;
18  import cz.vutbr.web.css.TermIdent;
19  import cz.vutbr.web.css.TermInteger;
20  import cz.vutbr.web.css.TermString;
21  
22  // we use some classes from braille-css because they are not available in jStyleParser (but they are
23  // not specific to braille CSS)
24  import org.daisy.braille.css.InlineStyle;
25  import org.daisy.braille.css.RuleCounterStyle;
26  
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  import org.unbescape.css.CssEscape;
31  
32  /**
33   * See http://www.w3.org/TR/css-counter-styles-3
34   */
35  public interface CounterStyle {
36  
37  	public String format(int counterValue);
38  
39  	public String format(int counterValue, boolean withPrefixAndSuffix);
40  
41  	@Deprecated
42  	public String getTextTransform(int counterValue);
43  
44  	// numeric
45  	public final static CounterStyle DECIMAL = CounterStyleImpl.predefinedCounterStyles.get("decimal");
46  	// alphabetic
47  	public final static CounterStyle LOWER_ALPHA = CounterStyleImpl.predefinedCounterStyles.get("lower-alpha");
48  	public final static CounterStyle UPPER_ALPHA = CounterStyleImpl.predefinedCounterStyles.get("upper-alpha");
49  	public final static CounterStyle LOWER_ROMAN = CounterStyleImpl.predefinedCounterStyles.get("lower-roman");
50  	public final static CounterStyle UPPER_ROMAN = CounterStyleImpl.predefinedCounterStyles.get("upper-roman");
51  	// cyclic
52  	public final static CounterStyle DISC = CounterStyleImpl.predefinedCounterStyles.get("disc");
53  
54  	/**
55  	 * Create a CounterStyle from a <code>symbols()</code> function
56  	 */
57  	public static CounterStyle fromSymbolsFunction(Term<?> term) throws IllegalArgumentException {
58  		return CounterStyleImpl.fromSymbolsFunction(term);
59  	}
60  
61  	/**
62  	 * Create a map of named CounterStyle from a set of <code>@counter-style</code> rules.
63  	 */
64  	public static Map<String,CounterStyle> parseCounterStyleRules(Iterable<RuleCounterStyle> style) {
65  		return (Map<String,CounterStyle>)(Map)CounterStyleImpl.parseCounterStyleRules(style);
66  	}
67  
68  	/**
69  	 * Return the predefined counter style with the given name, or {@code null} if no counter style
70  	 * with that name exists.
71  	 */
72  	public static CounterStyle predefined(String name) {
73  		return CounterStyleImpl.predefinedCounterStyles.get(name);
74  	}
75  
76  	/**
77  	 * Create a constant CounterStyle (cyclic with a single symbol)
78  	 */
79  	public static CounterStyle constant(String symbol) {
80  		return new CounterStyleImpl(symbol);
81  	}
82  
83  	/**
84  	 * Create a cyclic CounterStyle
85  	 */
86  	public static CounterStyle cycle(Iterable<String> symbols) {
87  		return new CounterStyleImpl(ImmutableList.copyOf(symbols));
88  	}
89  }
90  
91  class CounterStyleImpl implements CounterStyle {
92  
93  	private enum System {
94  		ALPHABETIC,
95  		NUMERIC,
96  		CYCLIC,
97  		FIXED,
98  		SYMBOLIC,
99  		ADDITIVE
100 	};
101 
102 	// package private for tests
103 	static class AdditiveTuple {
104 		final int weight;
105 		final String symbol;
106 		AdditiveTuple(int weight, String symbol) {
107 			this.weight = weight;
108 			this.symbol = symbol;
109 		}
110 	}
111 
112 	private final static Logger logger = LoggerFactory.getLogger(CounterStyle.class);
113 
114 	final static Map<String,CounterStyleImpl> predefinedCounterStyles = parseCounterStyleRules(
115 		Iterables.filter(
116 			new InlineStyle(
117 				"@counter-style decimal {"                                    + "\n" +
118 				"   system: numeric;"                                         + "\n" +
119 				"   symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';"        + "\n" +
120 				"   negative: '-';"                                           + "\n" +
121 				"}"                                                           + "\n" +
122 				"@counter-style lower-alpha {"                                + "\n" +
123 				"   system: alphabetic;"                                      + "\n" +
124 				"   symbols: 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j'"         + "\n" +
125 				"            'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't'"         + "\n" +
126 				"            'u' 'v' 'w' 'x' 'y' 'z';"                        + "\n" +
127 				"}"                                                           + "\n" +
128 				"@counter-style upper-alpha {"                                + "\n" +
129 				"   system: alphabetic;"                                      + "\n" +
130 				"   symbols: 'A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J'"         + "\n" +
131 				"            'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R' 'S' 'T'"         + "\n" +
132 				"            'U' 'V' 'W' 'X' 'Y' 'Z';"                        + "\n" +
133 				"}"                                                           + "\n" +
134 				"@counter-style lower-roman {"                                + "\n" +
135 				"   system: additive;"                                        + "\n" +
136 				"   range: 1 3999;"                                           + "\n" +
137 				"   additive-symbols: 1000 'm', 900 'cm', 500 'd', 400 'cd'," + "\n" +
138 				"                     100 'c', 90 'xc', 50 'l', 40 'xl',"     + "\n" +
139 				"                     10 'x', 9 'ix', 5 'v', 4 'iv', 1 'i';"  + "\n" +
140 				"}"                                                           + "\n" +
141 				"@counter-style upper-roman {"                                + "\n" +
142 				"   system: additive;"                                        + "\n" +
143 				"   range: 1 3999;"                                           + "\n" +
144 				"   additive-symbols: 1000 'M', 900 'CM', 500 'D', 400 'CD'," + "\n" +
145 				"                     100 'C', 90 'XC', 50 'L', 40 'XL',"     + "\n" +
146 				"                     10 'X', 9 'IX', 5 'V', 4 'IV', 1 'I';"  + "\n" +
147 				"}"                                                           + "\n" +
148 				"@counter-style disc {"                                       + "\n" +
149 				"   system: cyclic;"                                          + "\n" +
150 				"   symbols: '\\2022';"                                       + "\n" +
151 				"   suffix: ' ';"                                             + "\n" +
152 				"}"
153 			), RuleCounterStyle.class));
154 
155 	private final System system;
156 	private final List<String> symbols;
157 	private final List<AdditiveTuple> additiveSymbols;
158 	private final String negative;
159 	private final Supplier<CounterStyleImpl> fallback;
160 	private final String textTransform; // for braille CSS
161 	private final String prefix;
162 	private final String suffix;
163 
164 	/**
165 	 * Create a CounterStyle from a <code>symbols()</code> function
166 	 *
167 	 * @param term assumed to be a "symbols" function
168 	 */
169     CounterStyleImpl(TermFunction term) throws IllegalArgumentException {
170 		System system;
171 		List<Term<?>> symbols;
172 		if (term.size() > 0 && term.get(0) instanceof TermIdent) {
173 			system = readSystem((TermIdent)term.get(0));
174 			if (system == System.ADDITIVE)
175 				throw new IllegalArgumentException("system 'additive' not supported in symbols() function");
176 			symbols = term.subList(1, term.size());
177 		} else {
178 			system = System.SYMBOLIC;
179 			symbols = term;
180 		}
181 		this.symbols = readSymbols(symbols);
182 		this.additiveSymbols = null;
183 		this.system = system;
184 		this.negative = "-";
185 		this.fallback = () -> (CounterStyleImpl)DECIMAL;
186 		this.textTransform = "auto";
187 		this.prefix = "";
188 		this.suffix = " ";
189 	}
190 
191 	/**
192 	 * Create a CounterStyle from a <code>@counter-style</code> rule
193 	 */
194 	// FIXME: the "range" descriptor is not taken into account
195     CounterStyleImpl(RuleCounterStyle rule, Map<String,CounterStyleImpl> fallbacks) throws IllegalArgumentException {
196 		Declaration systemDecl = null;
197 		Declaration symbolsDecl = null;
198 		Declaration additiveSymbolsDecl = null;
199 		Declaration negativeDecl = null;
200 		Declaration fallbackDecl = null;
201 		Declaration prefixDecl = null;
202 		Declaration suffixDecl = null;
203 		Declaration textTransformDecl = null;
204 		for (Declaration d : rule) {
205 			String prop = d.getProperty();
206 			if ("system".equals(prop)) {
207 				if (systemDecl == null) systemDecl = d;
208 			} else if ("symbols".equals(prop)) {
209 				if (symbolsDecl == null) symbolsDecl = d;
210 			} else if ("additive-symbols".equals(prop)) {
211 				if (additiveSymbolsDecl == null) additiveSymbolsDecl = d;
212 			} else if ("negative".equals(prop)) {
213 				if (negativeDecl == null) negativeDecl = d;
214 			} else if ("fallback".equals(prop)) {
215 				if (fallbackDecl == null) fallbackDecl = d;
216 			} else if ("prefix".equals(prop)) {
217 				if (prefixDecl == null) prefixDecl = d;
218 			} else if ("suffix".equals(prop)) {
219 				if (suffixDecl == null) suffixDecl = d;
220 			} else if ("text-transform".equals(prop)) {
221 				if (textTransformDecl == null) textTransformDecl = d;
222 			}
223 		}
224 		System system; {
225 			system = null;
226 			if (systemDecl != null)
227 				try {
228 					system = readSystem(systemDecl);
229 				} catch (IllegalArgumentException e) {
230 					logger.warn(e.getMessage());
231 				}
232 			if (system == null) system = System.SYMBOLIC;
233 		}
234 		List<String> symbols; {
235 			if (system != System.ADDITIVE) {
236 				if (symbolsDecl != null)
237 					symbols = readSymbols(symbolsDecl);
238 				else
239 					throw new IllegalArgumentException("Missing symbols descriptor");
240 			} else
241 				symbols = null;
242 		}
243 		List<AdditiveTuple> additiveSymbols; {
244 			if (system == System.ADDITIVE) {
245 				if (additiveSymbolsDecl != null)
246 					additiveSymbols = readAdditiveSymbols(additiveSymbolsDecl);
247 				else
248 					throw new IllegalArgumentException("Missing symbols descriptor");
249 			} else
250 				additiveSymbols = null;
251 		}
252 		String negative; {
253 			negative = null;
254 			if (negativeDecl != null)
255 				try {
256 					negative = readSingleString("negative", negativeDecl).getValue();
257 				} catch (IllegalArgumentException e) {
258 					logger.warn(e.getMessage());
259 				}
260 			if (negative == null)
261 				negative = "-";
262 		}
263 		String fallback; {
264 			String ident = null;
265 			if (fallbackDecl != null)
266 				try {
267 					ident = readSingleIdent("fallback", fallbackDecl).getValue();
268 				} catch (IllegalArgumentException e) {
269 					logger.warn(e.getMessage());
270 				}
271 			if (ident == null)
272 				ident = "decimal";
273 			fallback = ident;
274 		}
275 		String textTransform; {
276 			textTransform = null;
277 			if (textTransformDecl != null) {
278 				logger.warn("'text-transform' descriptor in @counter-style rule is deprecated. "
279 				            + "Use a 'text-transform' property within the rule containing the 'content' property instead.");
280 				try {
281 					textTransform = readSingleIdent("text-transform", textTransformDecl).getValue();
282 				} catch (IllegalArgumentException e) {
283 					logger.warn(e.getMessage());
284 				}
285 			}
286 			if (textTransform == null)
287 				textTransform = "auto";
288 		}
289 		String prefix; {
290 			prefix = null;
291 			if (prefixDecl != null)
292 				try {
293 					prefix = readSingleString("prefix", prefixDecl).getValue();
294 				} catch (IllegalArgumentException e) {
295 					logger.warn(e.getMessage());
296 				}
297 			if (prefix == null)
298 				prefix = "";
299 		}
300 		String suffix; {
301 			suffix = null;
302 			if (suffixDecl != null)
303 				try {
304 					suffix = readSingleString("suffix", suffixDecl).getValue();
305 				} catch (IllegalArgumentException e) {
306 					logger.warn(e.getMessage());
307 				}
308 			if (suffix == null)
309 				suffix = ". ";
310 		}
311 		this.system = system;
312 		this.symbols = symbols;
313 		this.additiveSymbols = additiveSymbols;
314 		this.negative = negative;
315 		this.fallback = memoize(
316 			() -> {
317 				if (fallbacks != null && fallbacks.containsKey(fallback))
318 					return fallbacks.get(fallback);
319 				else if (predefinedCounterStyles.containsKey(fallback))
320 					return predefinedCounterStyles.get(fallback);
321 				else
322 					return (CounterStyleImpl)DECIMAL;
323 			}
324 		);
325 		this.textTransform = textTransform;
326 		this.prefix = prefix;
327 		this.suffix = suffix;
328 	}
329 
330 	/**
331 	 * Create a constant CounterStyle (cyclic with a single symbol)
332 	 */
333     CounterStyleImpl(String symbol) {
334 		this(Collections.singletonList(symbol));
335 	}
336 
337 	/**
338 	 * Create a cyclic CounterStyle
339 	 */
340     CounterStyleImpl(List<String> symbols) {
341 		system = System.CYCLIC;
342 		this.symbols = symbols;
343 		textTransform = "none";
344 		prefix = "";
345 		suffix = " ";
346 		additiveSymbols = null;
347 		negative = null;
348 		fallback = null;
349 	}
350 
351 	static final Map<String,CounterStyle> fromSymbolsCache = new HashMap<>();
352 
353 	static CounterStyle fromSymbolsFunction(Term<?> term) throws IllegalArgumentException {
354 		if (term instanceof TermFunction) {
355 			TermFunction function = (TermFunction)term;
356 			if (function.getFunctionName().equals("symbols")) {
357 				String k = function.toString();
358 				CounterStyle s = fromSymbolsCache.get(k);
359 				if (s == null) {
360 					s = new CounterStyleImpl(function);
361 					fromSymbolsCache.put(k, s);
362 				}
363 				return s;
364 			}
365 		}
366 		throw new IllegalArgumentException("argument must be a symbols() function");
367 	}
368 
369 	static Map<String,CounterStyleImpl> parseCounterStyleRules(Iterable<RuleCounterStyle> style) {
370 		Map<String,CounterStyleImpl> namedStyles = new HashMap<>();
371 		for (RuleCounterStyle rule : style)
372 			try {
373 				namedStyles.put(rule.getName(), new CounterStyleImpl(rule, namedStyles));
374 			} catch (IllegalArgumentException e) {
375 				logger.warn(e.getMessage());
376 			}
377 		return namedStyles;
378 	}
379 
380 	@Override
381 	public String format(int counterValue) {
382 		Object o = formatOrFallback(counterValue);
383 		if (o instanceof String)
384 			return (String)o;
385 		CounterStyleImpl fallback = (CounterStyleImpl)o;
386 		return fallback.format(counterValue);
387 	}
388 
389 	private Object formatOrFallback(int counterValue) {
390 		switch (system) {
391 		case ALPHABETIC: {
392 			Optional<String> formatted = counterRepresentationAlphabetic(counterValue, symbols);
393 			if (formatted.isPresent())
394 				return formatted.get();
395 			break; }
396 		case NUMERIC:
397 			return counterRepresentationNumeric(counterValue, symbols, negative);
398 		case CYCLIC:
399 			return counterRepresentationCyclic(counterValue, symbols);
400 		case FIXED: {
401 			Optional<String> formatted = counterRepresentationFixed(counterValue, symbols);
402 			if (formatted.isPresent())
403 				return formatted.get();
404 			break; }
405 		case SYMBOLIC: {
406 			Optional<String> formatted = counterRepresentationSymbolic(counterValue, symbols);
407 			if (formatted.isPresent())
408 				return formatted.get();
409 			break; }
410 		case ADDITIVE: {
411 			Optional<String> formatted = counterRepresentationAdditive(counterValue, additiveSymbols);
412 			if (formatted.isPresent())
413 				return formatted.get();
414 			break; }
415 		default:
416 			throw new IllegalStateException(); // can not happen
417 		}
418 		if (fallback.get() == null || fallback.get() == this)
419 			throw new IllegalStateException(); // can not happen
420 		return fallback.get();
421 	}
422 
423 	@Override
424 	public String format(int counterValue, boolean withPrefixAndSuffix) {
425 		// prefix and suffix always come from the specified counter-style, even if the actual
426 		// representation is constructed by a fallback style
427 		if (withPrefixAndSuffix)
428 			return prefix + format(counterValue) + suffix;
429 		else
430 			return format(counterValue);
431 	}
432 
433 	@Override
434 	public String getTextTransform(int counterValue) {
435 		Object o = formatOrFallback(counterValue);
436 		if (o instanceof String)
437 			return this.textTransform;;
438 		CounterStyleImpl fallback = (CounterStyleImpl)o;
439 		return fallback.textTransform;
440 	}
441 
442 	/* ============ private ========================================================= */
443 
444 	private static System readSystem(List<Term<?>> terms) throws IllegalArgumentException {
445 		return readSystem(readSingleIdent("system", terms));
446 	}
447 
448 	private static System readSystem(TermIdent ident) throws IllegalArgumentException {
449 		String system = ident.getValue();
450 		try {
451 			return System.valueOf(system.toUpperCase());
452 		} catch (IllegalArgumentException e) {
453 			throw new IllegalArgumentException("Invalid system: " + system);
454 		}
455 	}
456 
457 	private static List<String> readSymbols(List<Term<?>> terms) throws IllegalArgumentException {
458 		List<String> symbols = new ArrayList<>();
459 		for (Term<?> t : terms)
460 			symbols.add(readSymbol(t));
461 		if (symbols.isEmpty())
462 			throw new IllegalArgumentException("Empty symbols list");
463 		return symbols;
464 	}
465 
466 	private static List<AdditiveTuple> readAdditiveSymbols(List<Term<?>> terms) throws IllegalArgumentException {
467 		List<AdditiveTuple> symbols = new ArrayList<>();
468 		Iterator<Term<?>> tt = terms.iterator();
469 		int prevWeight = -1;
470 		while (tt.hasNext()) {
471 			Term<?> t = tt.next();
472 			if (t instanceof TermInteger) {
473 				int weight = ((TermInteger)t).getIntValue();
474 				if (prevWeight >= 0 && weight > prevWeight)
475 					throw new IllegalArgumentException(
476 						"Invalid additive tuple: expected integer weight greater than " + prevWeight + " but got " + t);
477 				prevWeight = weight;
478 				if (tt.hasNext()) {
479 					t = tt.next();
480 					try {
481 						symbols.add(new AdditiveTuple(weight, readSymbol(t)));
482 					} catch (IllegalArgumentException e) {
483 						throw new IllegalArgumentException("Invalid additive tuple: expected symbol but got " + t, e);
484 					}
485 				} else
486 					throw new IllegalArgumentException("Invalid additive tuple: expected symbol");
487 			} else
488 				throw new IllegalArgumentException("Invalid additive tuple: expected integer weight but got " + t);
489 		}
490 		if (symbols.isEmpty())
491 			throw new IllegalArgumentException("Empty additive-symbols list");
492 		return symbols;
493 	}
494 
495 	private static String readSymbol(Term<?> term) {
496 		if (term instanceof TermString)
497 			return ((TermString)term).getValue();
498 		else if (term instanceof TermIdent)
499 			return CssEscape.unescapeCss(((TermIdent)term).getValue());
500 		else if (term instanceof TermInteger)
501 			return "" + ((TermInteger)term).getValue();
502 		else
503 			throw new IllegalArgumentException("Invalid symbol: " + term);
504 	}
505 
506 	private static TermIdent readSingleIdent(String property, List<Term<?>> terms) throws IllegalArgumentException {
507 		if (terms.size() != 1 || !(terms.get(0) instanceof TermIdent))
508 			throw new IllegalArgumentException("Invalid " + property + ": " + CssSerializer.serializeTermList(terms));
509 		return (TermIdent)terms.get(0);
510 	}
511 
512 	private static TermString readSingleString(String property, List<Term<?>> terms) throws IllegalArgumentException {
513 		if (terms.size() != 1 || !(terms.get(0) instanceof TermString))
514 			throw new IllegalArgumentException("Invalid " + property + ": " + CssSerializer.serializeTermList(terms));
515 		return (TermString)terms.get(0);
516 	}
517 
518 	private static int mod(int a, int n) {
519 		int result = a % n;
520 		if (result < 0)
521 			result += n;
522 		return result;
523 	}
524 
525 	private static <T> Supplier<T> memoize(final Supplier<T> supplier) {
526 		return new Memoize<T>() {
527 			protected T _get() {
528 				return supplier.get();
529 			}
530 			@Override
531 			public String toString() {
532 				return "memoize( " + supplier + " )";
533 			}
534 		};
535 	}
536 
537 	private static abstract class Memoize<T> implements Supplier<T> {
538 		private boolean computed = false;
539 		private T value = null;
540 		protected abstract T _get();
541 		public final T get() {
542 			if (!computed) {
543 				value = _get();
544 				computed = true;
545 			}
546 			return value;
547 		}
548 	}
549 
550 	/* ============ package private for tests ======================================= */
551 
552 	static Optional<String> counterRepresentationAlphabetic(int counterValue, List<String> symbols) {
553 		if (counterValue < 1)
554 			return Optional.empty();
555 		if (counterValue > symbols.size())
556 			return Optional.of(
557 				counterRepresentationAlphabetic((counterValue - 1) / symbols.size(), symbols).get()
558 					+ symbols.get(mod(counterValue - 1, symbols.size())));
559 		else
560 			return Optional.of(symbols.get(counterValue - 1));
561 	}
562 
563 	static String counterRepresentationCyclic(int counterValue, List<String> symbols) {
564 		return symbols.get(mod(counterValue - 1, symbols.size()));
565 	}
566 
567 	static Optional<String> counterRepresentationFixed(int counterValue, List<String> symbols) {
568 		if (counterValue < 1 || counterValue > symbols.size())
569 			return Optional.empty();
570 		else
571 			return Optional.of(symbols.get(counterValue - 1));
572 	}
573 
574 	static String counterRepresentationNumeric(int counterValue, List<String> symbols, String negative) {
575 		if (counterValue < 0)
576 			return negative + counterRepresentationNumeric(- counterValue, symbols, negative);
577 		if (counterValue >= symbols.size())
578 			return counterRepresentationNumeric(counterValue / symbols.size(), symbols, negative)
579 				+ symbols.get(mod(counterValue, symbols.size()));
580 		else
581 			return symbols.get(counterValue);
582 	}
583 
584 	static Optional<String> counterRepresentationSymbolic(int counterValue, List<String> symbols) {
585 		if (counterValue < 1)
586 			return Optional.empty();
587 		String symbol = symbols.get(mod(counterValue - 1, symbols.size()));
588 		String s = symbol;
589 		for (int i = 0; i < ((counterValue - 1) / symbols.size()); i++)
590 			s += symbol;
591 		return Optional.of(s);
592 	}
593 
594 	static Optional<String> counterRepresentationAdditive(int counterValue, List<AdditiveTuple> symbols) {
595 		if (counterValue < 0)
596 			return Optional.empty();
597 		int rest = counterValue;
598 		String s = "";
599 		for (AdditiveTuple tuple : symbols) {
600 			if (tuple.weight == 0) {
601 				if (rest == 0 && s.isEmpty())
602 					s += tuple.symbol;
603 			} else {
604 				for (int i = 0; i < rest / tuple.weight; i++)
605 					s += tuple.symbol;
606 				rest = mod(rest, tuple.weight);
607 				if (rest == 0 && !s.isEmpty())
608 					break;
609 			}
610 		}
611 		if (rest != 0 || s.isEmpty())
612 			return Optional.empty();
613 		return Optional.of(s);
614 	}
615 }