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
23
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
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
45 public final static CounterStyle DECIMAL = CounterStyleImpl.predefinedCounterStyles.get("decimal");
46
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
52 public final static CounterStyle DISC = CounterStyleImpl.predefinedCounterStyles.get("disc");
53
54
55
56
57 public static CounterStyle fromSymbolsFunction(Term<?> term) throws IllegalArgumentException {
58 return CounterStyleImpl.fromSymbolsFunction(term);
59 }
60
61
62
63
64 public static Map<String,CounterStyle> parseCounterStyleRules(Iterable<RuleCounterStyle> style) {
65 return (Map<String,CounterStyle>)(Map)CounterStyleImpl.parseCounterStyleRules(style);
66 }
67
68
69
70
71
72 public static CounterStyle predefined(String name) {
73 return CounterStyleImpl.predefinedCounterStyles.get(name);
74 }
75
76
77
78
79 public static CounterStyle constant(String symbol) {
80 return new CounterStyleImpl(symbol);
81 }
82
83
84
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
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;
161 private final String prefix;
162 private final String suffix;
163
164
165
166
167
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
193
194
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
332
333 CounterStyleImpl(String symbol) {
334 this(Collections.singletonList(symbol));
335 }
336
337
338
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();
417 }
418 if (fallback.get() == null || fallback.get() == this)
419 throw new IllegalStateException();
420 return fallback.get();
421 }
422
423 @Override
424 public String format(int counterValue, boolean withPrefixAndSuffix) {
425
426
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
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
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 }