All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.daisy.pipeline.css.CounterStyle Maven / Gradle / Ivy

The newest version!
package org.daisy.pipeline.css;

import java.util.ArrayList;
import java.util.function.Supplier;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.google.common.collect.Iterables;

import cz.vutbr.web.css.Declaration;
import cz.vutbr.web.css.Term;
import cz.vutbr.web.css.TermFunction;
import cz.vutbr.web.css.TermIdent;
import cz.vutbr.web.css.TermInteger;
import cz.vutbr.web.css.TermString;

// we use some classes from braille-css because they are not available in jStyleParser (but they are
// not specific to braille CSS)
import org.daisy.braille.css.InlineStyle;
import org.daisy.braille.css.RuleCounterStyle;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.unbescape.css.CssEscape;

public class CounterStyle {

	private enum System {
		ALPHABETIC,
		NUMERIC,
		CYCLIC,
		FIXED,
		SYMBOLIC,
		ADDITIVE
	};

	private static class AdditiveTuple {
		final int weight;
		final String symbol;
		AdditiveTuple(int weight, String symbol) {
			this.weight = weight;
			this.symbol = symbol;
		}
	}

	private final static Logger logger = LoggerFactory.getLogger(CounterStyle.class);

	private final static Map predefinedCounterStyles = parseCounterStyleRules(
		Iterables.filter(
			new InlineStyle(
				"@counter-style decimal {"                                    + "\n" +
				"   system: numeric;"                                         + "\n" +
				"   symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';"        + "\n" +
				"   negative: '-';"                                           + "\n" +
				"}"                                                           + "\n" +
				"@counter-style lower-alpha {"                                + "\n" +
				"   system: alphabetic;"                                      + "\n" +
				"   symbols: 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j'"         + "\n" +
				"            'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't'"         + "\n" +
				"            'u' 'v' 'w' 'x' 'y' 'z';"                        + "\n" +
				"}"                                                           + "\n" +
				"@counter-style upper-alpha {"                                + "\n" +
				"   system: alphabetic;"                                      + "\n" +
				"   symbols: 'A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J'"         + "\n" +
				"            'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R' 'S' 'T'"         + "\n" +
				"            'U' 'V' 'W' 'X' 'Y' 'Z';"                        + "\n" +
				"}"                                                           + "\n" +
				"@counter-style lower-roman {"                                + "\n" +
				"   system: additive;"                                        + "\n" +
				"   range: 1 3999;"                                           + "\n" +
				"   additive-symbols: 1000 'm', 900 'cm', 500 'd', 400 'cd'," + "\n" +
				"                     100 'c', 90 'xc', 50 'l', 40 'xl',"     + "\n" +
				"                     10 'x', 9 'ix', 5 'v', 4 'iv', 1 'i';"  + "\n" +
				"}"                                                           + "\n" +
				"@counter-style upper-roman {"                                + "\n" +
				"   system: additive;"                                        + "\n" +
				"   range: 1 3999;"                                           + "\n" +
				"   additive-symbols: 1000 'M', 900 'CM', 500 'D', 400 'CD'," + "\n" +
				"                     100 'C', 90 'XC', 50 'L', 40 'XL',"     + "\n" +
				"                     10 'X', 9 'IX', 5 'V', 4 'IV', 1 'I';"  + "\n" +
				"}"                                                           + "\n" +
				"@counter-style disc {"                                       + "\n" +
				"   system: cyclic;"                                          + "\n" +
				"   symbols: '\\2022';"                                       + "\n" +
				"   suffix: ' ';"                                             + "\n" +
				"}"
			), RuleCounterStyle.class));

	// numeric
	public final static CounterStyle DECIMAL = predefinedCounterStyles.get("decimal");
	// alphabetic
	public final static CounterStyle LOWER_ALPHA = predefinedCounterStyles.get("lower-alpha");
	public final static CounterStyle UPPER_ALPHA = predefinedCounterStyles.get("upper-alpha");
	public final static CounterStyle LOWER_ROMAN = predefinedCounterStyles.get("lower-roman");
	public final static CounterStyle UPPER_ROMAN = predefinedCounterStyles.get("upper-roman");
	// cyclic
	public final static CounterStyle DISC = predefinedCounterStyles.get("disc");

	private final System system;
	private final List symbols;
	private final List additiveSymbols;
	private final String negative;
	private final Supplier fallback;
	private final String prefix;
	private final String suffix;

	/**
	 * Create a CounterStyle from a symbols() function
	 *
	 * @param term assumed to be a "symbols" function
	 */
	private CounterStyle(TermFunction term) throws IllegalArgumentException {
		System system;
		List> symbols;
		if (term.size() > 0 && term.get(0) instanceof TermIdent) {
			system = readSystem((TermIdent)term.get(0));
			if (system == System.ADDITIVE)
				throw new IllegalArgumentException("system 'additive' not supported in symbols() function");
			symbols = term.subList(1, term.size());
		} else {
			system = System.SYMBOLIC;
			symbols = term;
		}
		this.symbols = readSymbols(symbols);
		this.additiveSymbols = null;
		this.system = system;
		this.negative = "-";
		this.fallback = () -> DECIMAL;
		this.prefix = "";
		this.suffix = " ";
	}

	/**
	 * Create a CounterStyle from a @counter-style rule
	 */
	// FIXME: the "range" descriptor is not taken into account
	// FIXME: the "text-transform" descriptor (from braille CSS) is not taken into account
	private CounterStyle(RuleCounterStyle rule, Map fallbacks) throws IllegalArgumentException {
		Declaration systemDecl = null;
		Declaration symbolsDecl = null;
		Declaration additiveSymbolsDecl = null;
		Declaration negativeDecl = null;
		Declaration fallbackDecl = null;
		Declaration prefixDecl = null;
		Declaration suffixDecl = null;
		for (Declaration d : rule) {
			String prop = d.getProperty();
			if ("system".equals(prop)) {
				if (systemDecl == null) systemDecl = d;
			} else if ("symbols".equals(prop)) {
				if (symbolsDecl == null) symbolsDecl = d;
			} else if ("additive-symbols".equals(prop)) {
				if (additiveSymbolsDecl == null) additiveSymbolsDecl = d;
			} else if ("negative".equals(prop)) {
				if (negativeDecl == null) negativeDecl = d;
			} else if ("fallback".equals(prop)) {
				if (fallbackDecl == null) fallbackDecl = d;
			} else if ("prefix".equals(prop)) {
				if (prefixDecl == null) prefixDecl = d;
			} else if ("suffix".equals(prop)) {
				if (suffixDecl == null) suffixDecl = d;
			}
		}
		System system; {
			system = null;
			if (systemDecl != null)
				try {
					system = readSystem(systemDecl);
				} catch (IllegalArgumentException e) {
					logger.warn(e.getMessage());
				}
			if (system == null) system = System.SYMBOLIC;
		}
		List symbols; {
			if (system != System.ADDITIVE) {
				if (symbolsDecl != null)
					symbols = readSymbols(symbolsDecl);
				else
					throw new IllegalArgumentException("Missing symbols descriptor");
			} else
				symbols = null;
		}
		List additiveSymbols; {
			if (system == System.ADDITIVE) {
				if (additiveSymbolsDecl != null)
					additiveSymbols = readAdditiveSymbols(additiveSymbolsDecl);
				else
					throw new IllegalArgumentException("Missing symbols descriptor");
			} else
				additiveSymbols = null;
		}
		String negative; {
			negative = null;
			if (negativeDecl != null)
				try {
					negative = readSingleString("negative", negativeDecl).getValue();
				} catch (IllegalArgumentException e) {
					logger.warn(e.getMessage());
				}
			if (negative == null)
				negative = "-";
		}
		String fallback; {
			String ident = null;
			if (fallbackDecl != null)
				try {
					ident = readSingleIdent("fallback", fallbackDecl).getValue();
				} catch (IllegalArgumentException e) {
					logger.warn(e.getMessage());
				}
			if (ident == null)
				ident = "decimal";
			fallback = ident;
		}
		String prefix; {
			prefix = null;
			if (prefixDecl != null)
				try {
					prefix = readSingleString("prefix", prefixDecl).getValue();
				} catch (IllegalArgumentException e) {
					logger.warn(e.getMessage());
				}
			if (prefix == null)
				prefix = "";
		}
		String suffix; {
			suffix = null;
			if (suffixDecl != null)
				try {
					suffix = readSingleString("suffix", suffixDecl).getValue();
				} catch (IllegalArgumentException e) {
					logger.warn(e.getMessage());
				}
			if (suffix == null)
				suffix = ". "; // note that for braille CSS this should be the empty string
		}
		this.system = system;
		this.symbols = symbols;
		this.additiveSymbols = additiveSymbols;
		this.negative = negative;
		this.fallback = memoize(
			() -> {
				if (fallbacks != null && fallbacks.containsKey(fallback))
					return fallbacks.get(fallback);
				else if (predefinedCounterStyles.containsKey(fallback))
					return predefinedCounterStyles.get(fallback);
				else
					return DECIMAL;
			}
		);
		this.prefix = prefix;
		this.suffix = suffix;
	}

	private static final Map fromSymbolsCache = new HashMap<>();

	/**
	 * Create a CounterStyle from a symbols() function
	 */
	public static CounterStyle fromSymbolsFunction(Term term) throws IllegalArgumentException {
		if (term instanceof TermFunction) {
			TermFunction function = (TermFunction)term;
			if (function.getFunctionName().equals("symbols")) {
				String k = function.toString();
				CounterStyle s = fromSymbolsCache.get(k);
				if (s == null) {
					s = new CounterStyle(function);
					fromSymbolsCache.put(k, s);
				}
				return s;
			}
		}
		throw new IllegalArgumentException("argument must be a symbols() function");
	}

	/**
	 * Create a map of named CounterStyle from a set of @counter-style rules.
	 */
	public static Map parseCounterStyleRules(Iterable style) {
		Map namedStyles = new HashMap<>();
		for (RuleCounterStyle rule : style)
			try {
				namedStyles.put(rule.getName(), new CounterStyle(rule, namedStyles));
			} catch (IllegalArgumentException e) {
				logger.warn(e.getMessage());
			}
		return namedStyles;
	}

	public String format(int counterValue) {
		switch (system) {
		case ALPHABETIC: {
			Optional formatted = counterRepresentationAlphabetic(counterValue, symbols);
			if (formatted.isPresent())
				return formatted.get();
			break; }
		case NUMERIC:
			return counterRepresentationNumeric(counterValue, symbols, negative);
		case CYCLIC:
			return counterRepresentationCyclic(counterValue, symbols);
		case FIXED: {
			Optional formatted = counterRepresentationFixed(counterValue, symbols);
			if (formatted.isPresent())
				return formatted.get();
			break; }
		case SYMBOLIC: {
			Optional formatted = counterRepresentationSymbolic(counterValue, symbols);
			if (formatted.isPresent())
				return formatted.get();
			break; }
		case ADDITIVE: {
			Optional formatted = counterRepresentationAdditive(counterValue, additiveSymbols);
			if (formatted.isPresent())
				return formatted.get();
			break; }
		default:
			throw new IllegalStateException(); // can not happen
		}
		if (fallback.get() == null || fallback.get() == this)
			throw new IllegalStateException(); // can not happen
		return fallback.get().format(counterValue);
	}

	public String format(int counterValue, boolean withPrefixAndSuffix) {
		// prefix and suffix always come from the specified counter-style, even if the actual
		// representation is constructed by a fallback style
		if (withPrefixAndSuffix)
			return prefix + format(counterValue) + suffix;
		else
			return format(counterValue);
	}

	/* ============ private ========================================================= */

	private static System readSystem(List> terms) throws IllegalArgumentException {
		return readSystem(readSingleIdent("system", terms));
	}

	private static System readSystem(TermIdent ident) throws IllegalArgumentException {
		String system = ident.getValue();
		try {
			return System.valueOf(system.toUpperCase());
		} catch (IllegalArgumentException e) {
			throw new IllegalArgumentException("Invalid system: " + system);
		}
	}

	private static List readSymbols(List> terms) throws IllegalArgumentException {
		List symbols = new ArrayList<>();
		for (Term t : terms) {
			if (t instanceof TermString)
				symbols.add(((TermString)t).getValue());
			else if (t instanceof TermIdent)
				symbols.add(CssEscape.unescapeCss(((TermIdent)t).getValue()));
			else
				throw new IllegalArgumentException("Invalid symbol: " + t);
		}
		if (symbols.isEmpty())
			throw new IllegalArgumentException("Empty symbols list");
		return symbols;
	}

	private static List readAdditiveSymbols(List> terms) throws IllegalArgumentException {
		List symbols = new ArrayList<>();
		Iterator> tt = terms.iterator();
		int prevWeight = -1;
		while (tt.hasNext()) {
			Term t = tt.next();
			if (t instanceof TermInteger) {
				int weight = ((TermInteger)t).getIntValue();
				if (prevWeight >= 0 && weight > prevWeight)
					throw new IllegalArgumentException(
						"Invalid additive tuple: expected integer weight greater than " + prevWeight + " but got " + t);
				prevWeight = weight;
				if (tt.hasNext()) {
					t = tt.next();
					if (t instanceof TermString) {
						String symbol = ((TermString)t).getValue();
						symbols.add(new AdditiveTuple(weight, symbol));
					} else
						throw new IllegalArgumentException("Invalid additive tuple: expected symbol but got " + t);
				} else
					throw new IllegalArgumentException("Invalid additive tuple: expected symbol");
			} else
				throw new IllegalArgumentException("Invalid additive tuple: expected integer weight but got " + t);
		}
		if (symbols.isEmpty())
			throw new IllegalArgumentException("Empty additive-symbols list");
		return symbols;
	}

	private static TermIdent readSingleIdent(String property, List> terms) throws IllegalArgumentException {
		if (terms.size() != 1 || !(terms.get(0) instanceof TermIdent))
			throw new IllegalArgumentException("Invalid " + property + ": " + CssSerializer.serializeTermList(terms));
		return (TermIdent)terms.get(0);
	}

	private static TermString readSingleString(String property, List> terms) throws IllegalArgumentException {
		if (terms.size() != 1 || !(terms.get(0) instanceof TermString))
			throw new IllegalArgumentException("Invalid " + property + ": " + CssSerializer.serializeTermList(terms));
		return (TermString)terms.get(0);
	}

	private static int mod(int a, int n) {
		int result = a % n;
		if (result < 0)
			result += n;
		return result;
	}

	private static  Supplier memoize(final Supplier supplier) {
		return new Memoize() {
			protected T _get() {
				return supplier.get();
			}
			@Override
			public String toString() {
				return "memoize( " + supplier + " )";
			}
		};
	}

	private static abstract class Memoize implements Supplier {
		private boolean computed = false;
		private T value = null;
		protected abstract T _get();
		public final T get() {
			if (!computed) {
				value = _get();
				computed = true;
			}
			return value;
		}
	}

	/* ============ package private for tests ======================================= */

	static Optional counterRepresentationAlphabetic(int counterValue, List symbols) {
		if (counterValue < 1)
			return Optional.empty();
		if (counterValue > symbols.size())
			return Optional.of(
				counterRepresentationAlphabetic((counterValue - 1) / symbols.size(), symbols).get()
					+ symbols.get(mod(counterValue - 1, symbols.size())));
		else
			return Optional.of(symbols.get(counterValue - 1));
	}

	static String counterRepresentationCyclic(int counterValue, List symbols) {
		return symbols.get(mod(counterValue - 1, symbols.size()));
	}

	static Optional counterRepresentationFixed(int counterValue, List symbols) {
		if (counterValue < 1 || counterValue > symbols.size())
			return Optional.empty();
		else
			return Optional.of(symbols.get(counterValue - 1));
	}

	static String counterRepresentationNumeric(int counterValue, List symbols, String negative) {
		if (counterValue < 0)
			return negative + counterRepresentationNumeric(- counterValue, symbols, negative);
		if (counterValue >= symbols.size())
			return counterRepresentationNumeric(counterValue / symbols.size(), symbols, negative)
				+ symbols.get(mod(counterValue, symbols.size()));
		else
			return symbols.get(counterValue);
	}

	static Optional counterRepresentationSymbolic(int counterValue, List symbols) {
		if (counterValue < 1)
			return Optional.empty();
		String symbol = symbols.get(mod(counterValue - 1, symbols.size()));
		String s = symbol;
		for (int i = 0; i < ((counterValue - 1) / symbols.size()); i++)
			s += symbol;
		return Optional.of(s);
	}

	static Optional counterRepresentationAdditive(int counterValue, List symbols) {
		if (counterValue < 0)
			return Optional.empty();
		int rest = counterValue;
		String s = "";
		for (AdditiveTuple tuple : symbols) {
			if (tuple.weight == 0) {
				if (rest == 0 && s.isEmpty())
					s += tuple.symbol;
			} else {
				for (int i = 0; i < rest / tuple.weight; i++)
					s += tuple.symbol;
				rest = mod(rest, tuple.weight);
				if (rest == 0 && !s.isEmpty())
					break;
			}
		}
		if (rest != 0 || s.isEmpty())
			return Optional.empty();
		return Optional.of(s);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy