1   package org.daisy.pipeline.braille.common;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.HashMap;
6   import java.util.Iterator;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.NoSuchElementException;
10  import java.util.function.Supplier;
11  
12  import org.daisy.braille.css.SimpleInlineStyle;
13  import org.daisy.braille.css.BrailleCSSProperty.Hyphens;
14  import org.daisy.braille.css.BrailleCSSProperty.TextTransform;
15  import org.daisy.dotify.api.translator.UnsupportedMetricException;
16  import org.daisy.pipeline.braille.common.Hyphenator.NonStandardHyphenationException;
17  import org.daisy.pipeline.braille.css.CSSStyledText;
18  
19  import com.google.common.base.MoreObjects;
20  import com.google.common.base.MoreObjects.ToStringHelper;
21  import com.google.common.collect.Iterables;
22  import com.google.common.collect.Maps;
23  
24  import cz.vutbr.web.css.CSSProperty;
25  import cz.vutbr.web.css.Term;
26  import cz.vutbr.web.css.TermIdent;
27  import cz.vutbr.web.css.TermList;
28  
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  /**
33   * {@link BrailleTranslator} that dispatches to sub-translators based on text-transform values.
34   */
35  public class CompoundBrailleTranslator extends AbstractBrailleTranslator {
36  
37  	private final Map<String,BrailleTranslator> translators;
38  	private final boolean implementsFromStyledTextToBraille;
39  	private final boolean implementsLineBreakingFromStyledText;
40  
41  	public CompoundBrailleTranslator(BrailleTranslator mainTranslator, Map<String,Supplier<BrailleTranslator>> subTranslators) {
42  		if (subTranslators.containsKey("auto") || mainTranslator == null)
43  			throw new IllegalArgumentException();
44  		translators = new HashMap<>();
45  		translators.put("auto", mainTranslator);
46  		for (Map.Entry<String,Supplier<BrailleTranslator>> t : subTranslators.entrySet())
47  			try {
48  				translators.put(t.getKey(), t.getValue().get());
49  			} catch (NoSuchElementException e) {
50  				logger.warn("No braille translator found for handling text-transform '" + t.getKey() + "'");
51  			}
52  		implementsFromStyledTextToBraille = Iterables.all(
53  			translators.values(),
54  			t -> {
55  				try {
56  					t.fromStyledTextToBraille();
57  					return true; }
58  				catch (UnsupportedOperationException e) {
59  					return false; }} );
60  		implementsLineBreakingFromStyledText = Iterables.all(
61  			translators.values(),
62  			t -> {
63  				try {
64  					t.lineBreakingFromStyledText();
65  					return true; }
66  				catch (UnsupportedOperationException e) {
67  					return false; }} );
68  	}
69  
70  	protected CompoundBrailleTranslator(CompoundBrailleTranslator from) {
71  		super(from);
72  		this.translators = from.translators;
73  		this.implementsFromStyledTextToBraille = from.implementsFromStyledTextToBraille;
74  		this.implementsLineBreakingFromStyledText = from.implementsLineBreakingFromStyledText;
75  	}
76  
77  	@Override
78  	public ToStringHelper toStringHelper() {
79  		return MoreObjects.toStringHelper("CompoundBrailleTranslator")
80  			.add("translators", translators);
81  	}
82  
83  	/**
84  	 * @throws UnsupportedOperationException if the main translator or one of the sub-translators's
85  	 *                                       {@code #withHyphenator()} method throws {@link UnsupportedOperationException}
86  	 */
87  	public CompoundBrailleTranslator _withHyphenator(Hyphenator hyphenator) {
88  		Map<String,BrailleTranslator> translatorsWithHyphenator = new HashMap<>(); {
89  			for (String k : translators.keySet())
90  				translatorsWithHyphenator.put(k, translators.get(k).withHyphenator(hyphenator));
91  		}
92  		return new CompoundBrailleTranslator(translatorsWithHyphenator.remove("auto"),
93  		                                     Maps.transformValues(translatorsWithHyphenator, x -> (() -> x)));
94  	}
95  
96  	private static abstract class TransformImpl<T> {
97  
98  		abstract Iterable<T> transform(Iterable<CSSStyledText> styledText, int from, int to, String textTransform);
99  		abstract Iterable<CSSStyledText> transformContext(Iterable<CSSStyledText> styledText, int from, int to, String textTransform);
100 		
101 		abstract boolean supports(String textTransform);
102 
103 		List<T> transform(Iterable<CSSStyledText> styledText, int from, int to) {
104 			if (to < 0)
105 				to = Iterables.size(styledText);
106 			if (from < 0 || from > to)
107 				throw new IndexOutOfBoundsException();
108 			List<T> transformed = new ArrayList<>();
109 			if (from == to) return transformed;
110 			List<CSSStyledText> styledTextList = new ArrayList<>();
111 			for (CSSStyledText t : styledText)
112 				styledTextList.add(t.clone());
113 			// segments with same text-transform to be transformed next
114 			List<CSSStyledText> buffer = new ArrayList<CSSStyledText>();
115 			// already transformed segments (text-transform: none) to be used as context for next transformation
116 			List<CSSStyledText> context = new ArrayList<CSSStyledText>();
117 			String textTransform = "auto";
118 			for (int i = styledTextList.size(); i >= 0; i--) {
119 				CSSStyledText next;
120 				String nextTextTransform; {
121 					if (i == 0) {
122 						next = null;
123 						nextTextTransform = null;
124 					} else {
125 						next = styledTextList.get(i - 1);
126 						nextTextTransform = "auto";
127 						SimpleInlineStyle style = next.getStyle();
128 						if (style != null) {
129 							CSSProperty val = style.getProperty("text-transform");
130 							if (val != null) {
131 								if (val == TextTransform.NONE) {
132 									if (supports("none")) {
133 										nextTextTransform = "none";
134 										style.removeProperty("text-transform"); }
135 								} else if (val == TextTransform.list_values) {
136 									TermList values = style.getValue(TermList.class, "text-transform");
137 
138 									// According to the spec values should be "applied" from left to right, and
139 									// values of inner elements always come before values of outer elements (see
140 									// http://braillespecs.github.io/braille-css/#the-text-transform-property).
141 									// The way it works here is that the sub-translator that maps to the first
142 									// text-transform value (starting from left) is used to translate the segment,
143 									// and all values to the left and right are passed to the sub-translator.
144 									Iterator<Term<?>> it = values.iterator();
145 									while (it.hasNext()) {
146 										String tt = ((TermIdent)it.next()).getValue();
147 										if (supports(tt)) {
148 											nextTextTransform = tt;
149 											it.remove();
150 											break; }}
151 									if (values.isEmpty())
152 										style.removeProperty("text-transform"); }}}
153 					}
154 				}
155 				if (next == null || (nextTextTransform != textTransform && buffer.size() > 0)) {
156 					if (i < to) {
157 						int j = 0;
158 						for (T t : transform(org.daisy.pipeline.braille.common.util.Iterables.clone(
159 						                         Iterables.concat(buffer, context)),
160 						                     from > i ? from - i : 0,
161 						                     to < i + buffer.size() ? to - i : buffer.size(),
162 						                     textTransform))
163 							transformed.add(j++, t);
164 					}
165 					if (i <= from)
166 						return transformed;
167 					else {
168 						// set context for next transform
169 						try {
170 							int j = 0;
171 							for (CSSStyledText t : transformContext(org.daisy.pipeline.braille.common.util.Iterables.clone(
172 							                                            Iterables.concat(buffer, context)),
173 							                                        from > i ? from - i : 0,
174 							                                        buffer.size(),
175 							                                        textTransform))
176 								context.add(j++, t);
177 							buffer.clear();
178 						} catch (NonStandardHyphenationException e) {
179 							// try without hyphenation
180 							for (int j = buffer.size() - 1; j >= 0; j--) {
181 								CSSStyledText st = (CSSStyledText)buffer.get(j).clone();
182 								SimpleInlineStyle style = st.getStyle();
183 								if (style != null && style.getProperty("hyphens") == Hyphens.AUTO)
184 									style.removeProperty("hyphens");
185 								buffer.set(j, st);
186 								context.add(0, transformContext(org.daisy.pipeline.braille.common.util.Iterables.clone(
187 								                                    Iterables.concat(buffer, context)),
188 								                                j,
189 								                                j + 1,
190 								                                textTransform).iterator().next());
191 								buffer.remove(j);
192 							}
193 						}
194 					}
195 				}
196 				if (next != null) {
197 					buffer.add(0, next);
198 					textTransform = nextTextTransform;
199 				}
200 			}
201 			return transformed;
202 		}
203 	}
204 
205 	private FromStyledTextToBraille fromStyledTextToBraille = null;
206 
207 	/**
208 	 * @throws UnsupportedOperationException if the main translator or one of the sub-translators's
209 	 *                                       {@code #withHyphenator()} method throws {@link UnsupportedOperationException}
210 	 */
211 	@Override
212 	public FromStyledTextToBraille fromStyledTextToBraille() {
213 		if (!implementsFromStyledTextToBraille)
214 			throw new UnsupportedOperationException();
215 		if (fromStyledTextToBraille == null)
216 			fromStyledTextToBraille = new FromStyledTextToBraille() {
217 					TransformImpl<CSSStyledText> impl = new TransformImpl<CSSStyledText>() {
218 							Iterable<CSSStyledText> transform(Iterable<CSSStyledText> styledText, int from, int to, String textTransform) {
219 								return translators.get(textTransform).fromStyledTextToBraille().transform(styledText, from, to);
220 							}
221 							Iterable<CSSStyledText> transformContext(Iterable<CSSStyledText> styledText, int from, int to, String textTransform) {
222 								return transform(styledText, from, to, textTransform);
223 							}
224 							boolean supports(String textTransform) {
225 								return translators.containsKey(textTransform);
226 							}
227 						};
228 					public Iterable<CSSStyledText> transform(Iterable<CSSStyledText> styledText, int from, int to) {
229 						return impl.transform(styledText, from, to);
230 					}
231 					@Override
232 					public String toString() {
233 						return CompoundBrailleTranslator.this.toString();
234 					}
235 				};
236 		return fromStyledTextToBraille;
237 	}
238 
239 	private LineBreakingFromStyledText lineBreakingFromStyledText = null;
240 
241 	/**
242 	 * @throws UnsupportedOperationException if the main translator or one of the sub-translators's
243 	 *                                       {@code #withHyphenator()} method throws {@link UnsupportedOperationException}
244 	 */
245 	@Override
246 	public LineBreakingFromStyledText lineBreakingFromStyledText() {
247 		if (!implementsLineBreakingFromStyledText)
248 			throw new UnsupportedOperationException();
249 		if (lineBreakingFromStyledText == null)
250 			lineBreakingFromStyledText = new LineBreakingFromStyledText() {
251 					TransformImpl<LineIterator> impl = new TransformImpl<LineIterator>() {
252 							Iterable<LineIterator> transform(Iterable<CSSStyledText> styledText, int from, int to, String textTransform) {
253 								return Collections.singleton(
254 									translators.get(textTransform).lineBreakingFromStyledText().transform(styledText, from, to));
255 							}
256 							Iterable<CSSStyledText> transformContext(Iterable<CSSStyledText> styledText, int from, int to, String textTransform) {
257 								return translators.get(textTransform).fromStyledTextToBraille().transform(styledText, from, to);
258 							}
259 							boolean supports(String textTransform) {
260 								return translators.containsKey(textTransform);
261 							}
262 						};
263 					public LineIterator transform(Iterable<CSSStyledText> styledText, int from, int to) {
264 						return concatLineIterators(impl.transform(styledText, from, to));
265 					}
266 					@Override
267 					public String toString() {
268 						return CompoundBrailleTranslator.this.toString();
269 					}
270 				};
271 		return lineBreakingFromStyledText;
272 	}
273 
274 	/*
275 	 * Note that this function can not be used to concatenate any LineIterator. It is assumed that
276 	 * the LineIterator are translations of consecutive segments of the same string, where each
277 	 * translation has taken into account the whole context.
278 	 */
279 	static BrailleTranslator.LineIterator concatLineIterators(List<BrailleTranslator.LineIterator> iterators) {
280 		if (iterators.size() == 0)
281 			return new BrailleTranslator.LineIterator() {
282 				public String nextTranslatedRow(int limit, boolean force, boolean wholeWordsOnly) {
283 					return "";
284 				}
285 				public String getTranslatedRemainder() {
286 					return "";
287 				}
288 				public int countRemaining() {
289 					return 0;
290 				}
291 				public boolean hasNext() {
292 					return false;
293 				}
294 				public BrailleTranslator.LineIterator copy() {
295 					return this;
296 				}
297 				public boolean supportsMetric(String metric) {
298 					return false;
299 				}
300 				public double getMetric(String metric) {
301 					throw new UnsupportedMetricException("Metric not supported: " + metric);
302 				}
303 			};
304 		else if (iterators.size() == 1 && iterators.get(0) != null)
305 			return iterators.get(0);
306 		else
307 			return new ConcatLineIterators(iterators);
308 	}
309 
310 	private static class ConcatLineIterators implements BrailleTranslator.LineIterator {
311 
312 		final List<BrailleTranslator.LineIterator> iterators;
313 		BrailleTranslator.LineIterator current;
314 		int currentIndex = 0;
315 
316 		ConcatLineIterators(List<BrailleTranslator.LineIterator> iterators) {
317 			this.iterators = iterators;
318 			currentIndex = -1;
319 			current = null;
320 			computeCurrent();
321 		}
322 
323 		void computeCurrent() {
324 			while (current == null || !current.hasNext())
325 				if (currentIndex + 1 < iterators.size())
326 					current = iterators.get(++currentIndex);
327 				else {
328 					current = null;
329 					break; }
330 		}
331 
332 		public String nextTranslatedRow(int limit, boolean force, boolean wholeWordsOnly) {
333 			String row = "";
334 			BrailleTranslator.LineIterator prev = null;
335 			while (limit > row.length()) {
336 				if (current == null || current == prev) break;
337 				String s = current.nextTranslatedRow(limit - row.length(), force, wholeWordsOnly);
338 				if (s.isEmpty() && current.hasNext())
339 					break;
340 				else
341 					force = false;
342 				row += s;
343 				prev = current;
344 				computeCurrent(); }
345 			return row;
346 		}
347 
348 		public String getTranslatedRemainder() {
349 			String remainder = "";
350 			if (current == null) return remainder;
351 			for (int i = currentIndex; i < iterators.size(); i++)
352 				if (iterators.get(i) != null)
353 					remainder += iterators.get(i).getTranslatedRemainder();
354 			return remainder;
355 		}
356 
357 		public int countRemaining() {
358 			int remaining = 0;
359 			if (current == null) return remaining;
360 			for (int i = currentIndex; i < iterators.size(); i++)
361 				if (iterators.get(i) != null)
362 					remaining += iterators.get(i).countRemaining();
363 			return remaining;
364 		}
365 
366 		public boolean hasNext() {
367 			computeCurrent();
368 			return current != null;
369 		}
370 
371 		public ConcatLineIterators copy() {
372 			List<BrailleTranslator.LineIterator> iteratorsCopy = new ArrayList<>(iterators.size() - currentIndex);
373 			for (int i = currentIndex; i < iterators.size(); i++)
374 				if (iterators.get(i) != null)
375 					iteratorsCopy.add((BrailleTranslator.LineIterator)iterators.get(i).copy());
376 			return new ConcatLineIterators(iteratorsCopy);
377 		}
378 
379 		public boolean supportsMetric(String metric) {
380 			return false;
381 		}
382 
383 		public double getMetric(String metric) {
384 			throw new UnsupportedMetricException("Metric not supported: " + metric);
385 		}
386 	}
387 
388 	private static final Logger logger = LoggerFactory.getLogger(CompoundBrailleTranslator.class);
389 
390 }