1 package org.daisy.pipeline.braille.common;
2
3 import static java.lang.Math.min;
4 import java.util.ArrayList;
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.regex.Matcher;
11 import java.util.regex.Pattern;
12
13 import static com.google.common.collect.Iterators.concat;
14 import static com.google.common.collect.Iterators.peekingIterator;
15 import com.google.common.collect.ImmutableList;
16 import static com.google.common.collect.Lists.charactersOf;
17 import com.google.common.collect.PeekingIterator;
18
19 import cz.vutbr.web.css.CSSProperty;
20 import cz.vutbr.web.css.TermInteger;
21 import cz.vutbr.web.css.TermString;
22
23 import org.daisy.braille.css.BrailleCSSProperty.HyphenateCharacter;
24 import org.daisy.braille.css.BrailleCSSProperty.Hyphens;
25 import org.daisy.braille.css.BrailleCSSProperty.TextTransform;
26 import org.daisy.braille.css.BrailleCSSProperty.WhiteSpace;
27 import org.daisy.braille.css.BrailleCSSProperty.WordSpacing;
28 import org.daisy.braille.css.SimpleInlineStyle;
29 import org.daisy.dotify.api.table.BrailleConverter;
30 import org.daisy.dotify.api.translator.UnsupportedMetricException;
31 import org.daisy.dotify.api.translator.BrailleTranslatorResult;
32 import static org.daisy.pipeline.braille.common.util.Strings.extractHyphens;
33 import static org.daisy.pipeline.braille.common.util.Tuple2;
34 import org.daisy.pipeline.braille.css.CSSStyledText;
35
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 public abstract class AbstractBrailleTranslator extends AbstractTransform implements BrailleTranslator {
40
41 private final BrailleConverter brailleCharset;
42
43 private static final Logger logger = LoggerFactory.getLogger(AbstractBrailleTranslator.class);
44
45 protected AbstractBrailleTranslator() {
46 this(null, null);
47 }
48
49
50
51
52
53
54 protected AbstractBrailleTranslator(BrailleConverter brailleCharset) {
55 this(null, brailleCharset);
56 }
57
58 protected AbstractBrailleTranslator(Hyphenator hyphenator, BrailleConverter brailleCharset) {
59 this.brailleCharset = brailleCharset;
60 withHyphenatorCache = new HashMap<>();
61 withHyphenatorCache.put(hyphenator, this);
62 }
63
64
65
66
67
68
69 protected AbstractBrailleTranslator(AbstractBrailleTranslator from) {
70 this.brailleCharset = from.brailleCharset;
71 this.withHyphenatorCache = from.withHyphenatorCache;
72 }
73
74
75
76
77
78
79
80
81 public BrailleTranslator _withHyphenator(Hyphenator hyphenator) throws UnsupportedOperationException {
82 throw new UnsupportedOperationException();
83 }
84
85 private Map<Hyphenator,BrailleTranslator> withHyphenatorCache;
86
87 @Override
88 public final BrailleTranslator withHyphenator(Hyphenator hyphenator) throws UnsupportedOperationException {
89 BrailleTranslator t = null;
90 if (withHyphenatorCache == null)
91 withHyphenatorCache = new HashMap<>();
92 else
93 t = withHyphenatorCache.get(hyphenator);
94 if (t == null) {
95 t = _withHyphenator(hyphenator);
96 withHyphenatorCache.put(hyphenator, t);
97 }
98 return t;
99 }
100
101 @Override
102 public FromStyledTextToBraille fromStyledTextToBraille() throws UnsupportedOperationException {
103 throw new UnsupportedOperationException();
104 }
105
106 private LineBreakingFromStyledText lineBreakingFromStyledText = null;
107
108 @Override
109 public LineBreakingFromStyledText lineBreakingFromStyledText() throws UnsupportedOperationException {
110
111 if (lineBreakingFromStyledText == null) {
112 FromStyledTextToBraille fromStyledTextToBraille = fromStyledTextToBraille();
113 Character blankChar = brailleCharset == null
114 ? '\u2800'
115 : brailleCharset.toText("\u2800").toCharArray()[0];
116 Character defaultHyphenChar = brailleCharset == null
117 ? '\u2824'
118 : brailleCharset.toText("\u2824").toCharArray()[0];
119 lineBreakingFromStyledText = new StandardLineBreaker(fromStyledTextToBraille, blankChar, defaultHyphenChar, brailleCharset);
120 }
121 return lineBreakingFromStyledText;
122 }
123
124
125
126
127
128 private static class StandardLineBreaker extends util.DefaultLineBreaker {
129
130 private final FromStyledTextToBraille fromStyledTextToBraille;
131
132 public StandardLineBreaker(FromStyledTextToBraille fromStyledTextToBraille, char blankChar, char defaultHyphenChar, BrailleConverter brailleCharset) {
133 super(blankChar, defaultHyphenChar, brailleCharset, null);
134 this.fromStyledTextToBraille = fromStyledTextToBraille;
135 }
136
137 protected BrailleStream translateAndHyphenate(Iterable<CSSStyledText> styledText, int from, int to) {
138 List<String> braille = new ArrayList<>();
139 for (CSSStyledText t : fromStyledTextToBraille.transform(styledText)) {
140 String b = t.getText();
141 SimpleInlineStyle s = t.getStyle();
142 if (s != null) {
143 Hyphens h = s.getProperty("hyphens");
144 if (h == Hyphens.NONE || h == Hyphens.MANUAL) {
145 if (h == Hyphens.NONE)
146 b = b.replaceAll("[\u00AD\u200B]","");
147 s.removeProperty("hyphens"); }
148 WhiteSpace ws = s.getProperty("white-space");
149 if (ws != null) {
150 if (ws == WhiteSpace.PRE_WRAP)
151 b = b.replaceAll("[\\x20\t\\u2800]+", "$0\u200B")
152 .replaceAll("[\\x20\t\\u2800]", "\u00A0");
153 if (ws == WhiteSpace.PRE_WRAP || ws == WhiteSpace.PRE_LINE)
154 b = b.replaceAll("[\\n\\r]", "\u2028");
155 s.removeProperty("white-space"); }
156 TextTransform tt = s.getProperty("text-transform");
157 if (tt == TextTransform.NONE)
158 s.removeProperty("text-transform");
159 s.removeProperty("braille-charset");
160 for (String prop : s.getPropertyNames())
161 logger.warn("'{}: {}' not supported", prop, s.get(prop));
162 }
163 braille.add(b);
164 }
165 StringBuilder brailleString = new StringBuilder();
166 int fromChar = 0;
167 int toChar = to >= 0 ? 0 : -1;
168 for (String s : braille) {
169 brailleString.append(s);
170 if (--from == 0)
171 fromChar = brailleString.length();
172 if (--to == 0)
173 toChar = brailleString.length();
174 }
175 return new FullyHyphenatedAndTranslatedString(brailleString.toString(), fromChar, toChar);
176 }
177 }
178
179
180
181
182
183 public static abstract class util {
184
185 public static abstract class DefaultLineBreaker implements LineBreakingFromStyledText {
186
187 private final static char SHY = '\u00ad';
188 private final static char ZWSP = '\u200b';
189 private final static char SPACE = ' ';
190 private final static char CR = '\r';
191 private final static char LF = '\n';
192 private final static char TAB = '\t';
193 private final static char NBSP = '\u00a0';
194 private final static char BLANK = '\u2800';
195 private final static char LS = '\u2028';
196 private final static char RS = '\u001E';
197
198 private final char blankChar;
199
200 private final char defaultHyphenChar;
201 private final BrailleConverter brailleCharset;
202 private final Logger logger;
203
204 public DefaultLineBreaker() {
205 this(null);
206 }
207
208 public DefaultLineBreaker(Logger logger) {
209 this('\u2800', '\u2824', logger);
210 }
211
212 public DefaultLineBreaker(char blankChar, char defaultHyphenChar, Logger logger) {
213 this(blankChar, defaultHyphenChar, null, logger);
214 }
215
216
217
218
219
220
221
222
223
224 public DefaultLineBreaker(char blankChar, char defaultHyphenChar, BrailleConverter brailleCharset, Logger logger) {
225 this.blankChar = blankChar;
226 this.defaultHyphenChar = defaultHyphenChar;
227 this.brailleCharset = brailleCharset;
228 this.logger = logger;
229 }
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253 protected abstract BrailleStream translateAndHyphenate(Iterable<CSSStyledText> text, int from, int to);
254
255 protected interface BrailleStream extends Cloneable {
256 public boolean hasNext();
257
258
259
260
261
262
263 public String next(int limit, boolean force, boolean allowHyphens);
264 public Character peek();
265 public String remainder();
266 public Object clone();
267
268
269
270
271
272
273
274
275 public boolean hasPrecedingSpace();
276 }
277
278 public BrailleTranslator.LineIterator transform(final Iterable<CSSStyledText> text, int from, int to)
279 throws TransformationException {
280 List<Character> hyphenChars = new ArrayList<>();
281 int wordSpacing; {
282 wordSpacing = -1;
283 for (CSSStyledText st : text) {
284 SimpleInlineStyle style = st.getStyle();
285 char hyphenChar = defaultHyphenChar;
286 int spacing = 1;
287 if (style != null) {
288 CSSProperty val = style.getProperty("hyphenate-character");
289 if (val != null) {
290 if (val == HyphenateCharacter.braille_string) {
291 String s = style.getValue(TermString.class, "hyphenate-character").getValue();
292 if (s.length() == 1) {
293 hyphenChar = s.charAt(0);
294 if (brailleCharset != null)
295 hyphenChar = brailleCharset.toText("" + hyphenChar).toCharArray()[0];
296 } else
297 logger.warn("The 'hyphenate-character' property must be a single character, "
298 + "but got {}", s); }
299 style.removeProperty("hyphenate-character"); }
300 val = style.getProperty("word-spacing");
301 if (val != null) {
302 if (val == WordSpacing.length) {
303 spacing = style.getValue(TermInteger.class, "word-spacing").getIntValue();
304 if (spacing < 0) {
305 if (logger != null)
306 logger.warn("word-spacing: {} not supported, must be non-negative", val);
307 spacing = 1; }}
308
309
310 style.removeProperty("word-spacing"); }}
311 hyphenChars.add(hyphenChar);
312 if (wordSpacing < 0)
313 wordSpacing = spacing;
314 else if (wordSpacing != spacing)
315 throw new TransformationException("word-spacing must be constant, but both "
316 + wordSpacing + " and " + spacing + " specified"); }
317 if (wordSpacing < 0) wordSpacing = 1;
318 }
319 List<BrailleTranslator.LineIterator> lineIterators = new ArrayList<>();
320 Character hyphenChar = null;
321 if (to < 0) to = hyphenChars.size();
322 int i = from;
323 while (i < to) {
324 Character nextHyphenChar = hyphenChars.get(i);
325 if (hyphenChar != nextHyphenChar) {
326 if (i > from) {
327 lineIterators.add(
328 new LineIterator(translateAndHyphenate(text, from, i), blankChar, hyphenChar, wordSpacing));
329 from = i; }
330 hyphenChar = nextHyphenChar; }
331 i++; }
332 if (i > from)
333 lineIterators.add(
334 new LineIterator(translateAndHyphenate(text, from, i), blankChar, hyphenChar, wordSpacing));
335 return CompoundBrailleTranslator.concatLineIterators(lineIterators);
336 }
337
338 protected static class FullyHyphenatedAndTranslatedString implements BrailleStream {
339 private String next;
340 private String lastWordPart;
341 private String lastWordOtherPart;
342 private final boolean precedingSpace;
343 private final boolean alwaysEmpty;
344 private final char hyphenChar;
345
346 private final static Pattern WORD_BOUNDARY = Pattern.compile("[\\x20\t\\n\\r\\u2800\u2028]");
347 public FullyHyphenatedAndTranslatedString(String string) {
348 this(string, 0, -1);
349 }
350 public FullyHyphenatedAndTranslatedString(String string, int from, int to) {
351 this(string, from, to, SHY);
352 }
353 public FullyHyphenatedAndTranslatedString(String string, int from, int to, char hyphenChar) {
354
355
356
357
358
359
360 precedingSpace = DefaultLineBreaker.hasPrecedingSpace(string, from);
361 int len = string.length();
362 if (from < 0 || from > len)
363 throw new IllegalArgumentException();
364 next = string.substring(from);
365 lastWordPart = lastWordOtherPart = null;
366 if (to >= 0 && to != len) {
367 if (to > len || to < from)
368 throw new IllegalArgumentException();
369 to -= from;
370 len -= from;
371 int lastWordStart = lastIndexOf((" " + next.substring(0, to)), WORD_BOUNDARY);
372 if (lastWordStart == to) {
373 next = next.substring(0, to);
374 } else {
375 int lastWordEnd = to + indexOf((next.substring(to, len) + " "), WORD_BOUNDARY);
376 if (lastWordEnd == to) {
377 next = next.substring(0, to);
378 } else {
379 lastWordPart = next.substring(lastWordStart, to);
380 lastWordOtherPart = next.substring(to, lastWordEnd);
381 next = next.substring(0, lastWordStart);
382
383 lastWordPart = lastWordPart.replaceAll("^\u200b*", "");
384 if (lastWordPart.isEmpty())
385 lastWordPart = lastWordOtherPart = null;
386 }
387 }
388 } else {
389
390 next = next.replaceAll("\u00ad$", "");
391 }
392 if (next.replaceAll("[\u00ad\u200b]","").isEmpty())
393 next = null;
394 alwaysEmpty = (next == null);
395 this.hyphenChar = hyphenChar;
396 }
397 public boolean hasNext() {
398 return next != null || lastWordPart != null;
399 }
400 public String next(int limit, boolean force, boolean allowHyphens) {
401 if (next != null) {
402 String n = next;
403 next = null;
404 return n; }
405 else if (lastWordPart != null) {
406 if (force) {
407 String n = lastWordPart;
408 lastWordPart = null;
409 return n; }
410 Tuple2<String,byte[]> t = extractHyphens(lastWordPart + RS + lastWordOtherPart, false, SHY, ZWSP, RS);
411 String lastWord = t._1;
412
413 if (lastWord.length() <= limit) {
414 String n = lastWordPart;
415 lastWordPart = null;
416 return n; }
417
418
419 else if (allowHyphens) {
420 byte[] hyphens = t._2;
421 boolean inFirstPart = false;
422 String nextLastWordPart = null;
423 for (int i = limit; i > 0; i--) {
424
425 if ((hyphens[i - 1] & 4) == 4) {
426 inFirstPart = true;
427 nextLastWordPart = lastWord.substring(0, i); }
428
429 if ((((hyphens[i - 1] & 1) == 1) && (i < limit)) || ((hyphens[i - 1] & 2) == 2)) {
430 if (!inFirstPart) {
431
432
433 String n = lastWordPart;
434 lastWordPart = null;
435 return n; }
436 else {
437
438
439 String n = lastWord.substring(0, i);
440
441 if ((hyphens[i - 1] & 1) == 1)
442 n += hyphenChar;
443 lastWordPart = nextLastWordPart.substring(i);
444 if (lastWordPart.isEmpty()) lastWordPart = null;
445 return n; }}}}
446
447 return "";
448 }
449 else
450 throw new NoSuchElementException();
451 }
452 public Character peek() {
453 if (next != null)
454 return next.charAt(0);
455 else if (lastWordPart != null)
456 return lastWordPart.charAt(0);
457 else
458 throw new NoSuchElementException();
459 }
460 public String remainder() {
461 if (next == null && lastWordPart == null)
462 throw new NoSuchElementException();
463 StringBuilder remainder = new StringBuilder();
464 if (next != null)
465 remainder.append(next);
466 if (lastWordPart != null)
467 remainder.append(lastWordPart);
468 return remainder.toString();
469 }
470 public boolean hasPrecedingSpace() {
471 if (next == null && !alwaysEmpty)
472 throw new UnsupportedOperationException();
473 else
474 return precedingSpace;
475 }
476 @Override
477 public Object clone() {
478 try {
479 return super.clone();
480 } catch (CloneNotSupportedException e) {
481 throw new InternalError("coding error");
482 }
483 }
484
485
486
487 private static int indexOf(String string, Pattern pattern) {
488 Matcher m = pattern.matcher(string);
489 if (m.find())
490 return m.start();
491 else
492 return -1;
493 }
494
495
496
497 private static int lastIndexOf(String string, Pattern pattern) {
498 Matcher m = pattern.matcher(string);
499 int lastIndex = -1;
500 while (m.find())
501 lastIndex = m.start();
502 return lastIndex;
503 }
504 }
505
506 public static class LineIterator implements BrailleTranslator.LineIterator, Cloneable {
507
508 private final char blankChar;
509 private final char hyphenChar;
510 private final int wordSpacing;
511
512 public LineIterator(String fullyHyphenatedAndTranslatedString, char blankChar, char hyphenChar, int wordSpacing) {
513 this(fullyHyphenatedAndTranslatedString, 0, -1, blankChar, hyphenChar, wordSpacing);
514 }
515
516 public LineIterator(String fullyHyphenatedAndTranslatedString, int from, int to,
517 char blankChar, char hyphenChar, int wordSpacing) {
518 this(new FullyHyphenatedAndTranslatedString(fullyHyphenatedAndTranslatedString, from, to, hyphenChar),
519 blankChar, hyphenChar, wordSpacing);
520 }
521
522 public LineIterator(BrailleStream inputStream, char blankChar, char hyphenChar, int wordSpacing) {
523 this.inputStream = inputStream;
524 this.blankChar = blankChar;
525 this.hyphenChar = hyphenChar;
526 this.wordSpacing = wordSpacing;
527 this.lastCharIsSpace = inputStream.hasPrecedingSpace();
528 }
529
530 private BrailleStream inputStream;
531 private PeekingIterator<Character> inputBuffer = null;
532 private StringBuilder charBuffer = new StringBuilder();
533
534
535
536
537
538
539
540
541
542
543
544 private ArrayList<Byte> wrapInfo = new ArrayList<Byte>();
545
546 private final static byte NO_SOFT_WRAP = (byte)0x0;
547 private final static byte SOFT_WRAP_WITH_HYPHEN = (byte)0x1;
548 private final static byte SOFT_WRAP_WITHOUT_HYPHEN = (byte)0x3;
549 private final static byte SOFT_WRAP_AFTER_SPACE = (byte)0x7;
550 private final static byte HARD_WRAP = (byte)0x15;
551
552
553 private boolean lastCharIsSpace = false;
554 private int forcedBreakCount = 0;
555
556
557
558
559
560
561
562 private void fillRow(int limit, boolean force, boolean allowHyphens) {
563 int bufSize = charBuffer.length();
564 while (true) {
565 if (inputBuffer != null && !inputBuffer.hasNext()) {
566
567
568 if (bufSize > 0 && wrapInfo.get(bufSize - 1) != SOFT_WRAP_WITH_HYPHEN)
569 wrapInfo.set(bufSize - 1, (byte)(wrapInfo.get(bufSize - 1) | SOFT_WRAP_WITHOUT_HYPHEN));
570 inputBuffer = null;
571 }
572 if (inputBuffer == null) {
573 if (!inputStream.hasNext())
574 return;
575 if (bufSize < limit) {
576 String next = inputStream.next(limit - bufSize, force && (bufSize == 0), allowHyphens);
577 if (next.isEmpty())
578 return;
579 inputBuffer = peekingIterator(charactersOf(next).iterator()); }
580 if (inputBuffer == null) {
581 switch (inputStream.peek()) {
582 case SHY: case TAB:
583 case ZWSP: case BLANK:
584 case SPACE: case NBSP:
585 case LF: case LS:
586 case CR:
587 String next = inputStream.next(1, true, allowHyphens);
588 if (next.isEmpty())
589 throw new RuntimeException("coding error");
590 inputBuffer = peekingIterator(charactersOf(next).iterator());
591 break;
592 default:
593 return; }}}
594 char next = inputBuffer.peek();
595 switch (next) {
596 case SHY:
597 if (bufSize > 0)
598 wrapInfo.set(bufSize - 1, (byte)(wrapInfo.get(bufSize - 1) | SOFT_WRAP_WITH_HYPHEN));
599 lastCharIsSpace = false;
600 break;
601 case ZWSP:
602 if (bufSize > 0)
603 wrapInfo.set(bufSize - 1, (byte)(wrapInfo.get(bufSize - 1) | SOFT_WRAP_WITHOUT_HYPHEN));
604 break;
605 case SPACE:
606 case LF:
607 case CR:
608 case TAB:
609 case BLANK:
610 if (lastCharIsSpace)
611 break;
612 if (bufSize > 0)
613 wrapInfo.set(bufSize - 1, (byte)(wrapInfo.get(bufSize - 1) | SOFT_WRAP_WITHOUT_HYPHEN));
614 for (int i = 0; i < wordSpacing; i++) {
615 charBuffer.append(blankChar);
616 bufSize ++;
617 wrapInfo.add(SOFT_WRAP_AFTER_SPACE);
618 }
619 lastCharIsSpace = true;
620 break;
621 case NBSP:
622 charBuffer.append(blankChar);
623 bufSize ++;
624 wrapInfo.add(NO_SOFT_WRAP);
625 lastCharIsSpace = false;
626 break;
627 case LS:
628 if (bufSize > 0 && (wrapInfo.get(bufSize - 1) & HARD_WRAP) != HARD_WRAP)
629 wrapInfo.set(bufSize - 1, (byte)(wrapInfo.get(bufSize - 1) | HARD_WRAP));
630 else {
631
632
633 charBuffer.append(blankChar);
634 bufSize ++;
635 wrapInfo.add(HARD_WRAP);
636 }
637 lastCharIsSpace = true;
638 break;
639 default:
640 if (bufSize >= limit) return;
641 charBuffer.append(next);
642 bufSize ++;
643 wrapInfo.add(NO_SOFT_WRAP);
644 lastCharIsSpace = false; }
645 inputBuffer.next();
646 }
647 }
648
649
650
651
652
653 private void flushBuffers(int size) {
654 charBuffer = new StringBuilder(charBuffer.substring(size));
655 wrapInfo = new ArrayList<Byte>(wrapInfo.subList(size, wrapInfo.size()));
656 }
657
658
659
660
661
662
663
664
665
666 public String nextTranslatedRow(int limit, boolean force, boolean wholeWordsOnly) {
667 fillRow(limit, force, !wholeWordsOnly);
668 int bufSize = charBuffer.length();
669
670
671 if (bufSize == 0)
672 return "";
673
674
675 for (int i = 0; i < min(bufSize, limit); i++) {
676 if (wrapInfo.get(i) == HARD_WRAP) {
677 int cut = i + 1;
678
679
680 while (cut > 0 && charBuffer.charAt(cut - 1) == blankChar) cut--;
681
682
683 if (cut == 0)
684 cut = i + 1;
685 String rv = charBuffer.substring(0, cut);
686
687
688 cut = i + 1;
689 while (cut < bufSize && wrapInfo.get(cut) == SOFT_WRAP_AFTER_SPACE) cut++;
690 flushBuffers(cut);
691 return rv; }}
692
693
694 if (bufSize < limit) {
695 String rv = charBuffer.substring(0, bufSize);
696
697
698 if (wrapInfo.get(rv.length() - 1) == SOFT_WRAP_WITH_HYPHEN)
699 rv += hyphenChar;
700 charBuffer.setLength(0);
701 wrapInfo.clear();
702
703
704 int cut = bufSize;
705 while (cut > 0 && rv.charAt(cut - 1) == blankChar) cut--;
706
707
708
709
710 if (cut > 0 && cut < bufSize && hasNext())
711 rv = rv.substring(0, cut);
712 return rv; }
713
714
715 if (limit <= 0) {
716
717
718 int cut = 0;
719 while (cut < bufSize && wrapInfo.get(cut) == SOFT_WRAP_AFTER_SPACE) cut++;
720 flushBuffers(cut);
721 return ""; }
722
723
724
725 if ((wrapInfo.get(limit - 1) & SOFT_WRAP_WITHOUT_HYPHEN) == SOFT_WRAP_WITHOUT_HYPHEN) {
726 int cut = limit;
727
728
729 while (cut > 0 && charBuffer.charAt(cut - 1) == blankChar) cut--;
730
731
732 int cut2 = limit;
733 while (cut2 < bufSize && wrapInfo.get(cut2) == SOFT_WRAP_AFTER_SPACE) cut2++;
734 String rv = charBuffer.substring(0, cut2);
735 flushBuffers(cut2);
736
737
738 if (cut > 0 && cut < cut2 && hasNext())
739 rv = rv.substring(0, cut);
740 else if (cut2 > limit)
741 rv = rv.substring(0, limit);
742 return rv; }
743
744
745 for (int i = limit + 1; i - 1 < bufSize && charBuffer.charAt(i - 1) == blankChar; i++)
746 if ((wrapInfo.get(i - 1) & SOFT_WRAP_WITHOUT_HYPHEN) == SOFT_WRAP_WITHOUT_HYPHEN) {
747
748
749 int cut = limit;
750 while (cut > 0 && charBuffer.charAt(cut - 1) == blankChar) cut--;
751
752
753 int cut2 = i;
754 while (cut2 < bufSize && wrapInfo.get(cut2) == SOFT_WRAP_AFTER_SPACE) cut2++;
755 String rv = charBuffer.substring(0, cut2);
756 flushBuffers(cut2);
757
758
759 if (cut < cut2 && hasNext())
760 rv = rv.substring(0, cut);
761 else if (cut2 > limit)
762 rv = rv.substring(0, limit);
763 return rv; }
764
765
766 for (int i = limit - 1; i > 0; i--) {
767
768
769
770 if ((wrapInfo.get(i - 1) & SOFT_WRAP_WITHOUT_HYPHEN) == SOFT_WRAP_WITHOUT_HYPHEN
771 || (!wholeWordsOnly && (wrapInfo.get(i - 1) == SOFT_WRAP_WITH_HYPHEN))) {
772 int cut = i;
773 String rv;
774
775
776 if (wrapInfo.get(cut - 1) == 0x1) {
777 rv = charBuffer.substring(0, cut);
778 rv += hyphenChar; }
779 else {
780
781
782 while (cut > 0 && charBuffer.charAt(cut - 1) == blankChar) cut--;
783
784
785 if (cut == 0)
786 cut = i;
787 rv = charBuffer.substring(0, cut); }
788
789
790
791 cut = i;
792 while(cut < bufSize && charBuffer.charAt(cut) == blankChar) cut++;
793 flushBuffers(cut);
794 return rv; }}
795
796
797 if (force) {
798 forcedBreakCount++;
799 String rv = charBuffer.substring(0, limit);
800 flushBuffers(limit);
801 return rv; }
802
803 return "";
804 }
805
806
807
808
809 public boolean hasNext() {
810 return charBuffer.length() > 0
811 || (inputBuffer != null && inputBuffer.hasNext())
812 || inputStream.hasNext();
813 }
814
815
816
817
818 public String getTranslatedRemainder() {
819
820
821 BrailleStream save_inputStream = inputStream;
822 inputStream = emptyBrailleStream;
823 List<Character> save_inputBuffer = inputBuffer != null ? ImmutableList.copyOf(inputBuffer) : null;
824 if (save_inputBuffer != null) {
825 if (save_inputStream.hasNext()) {
826 inputBuffer = peekingIterator(
827 concat(save_inputBuffer.iterator(), charactersOf(save_inputStream.remainder()).iterator()));
828 } else {
829 inputBuffer = peekingIterator(save_inputBuffer.iterator());
830 }
831 } else if (save_inputStream.hasNext()) {
832 inputBuffer = peekingIterator(charactersOf(save_inputStream.remainder()).iterator());
833 } else {
834 inputBuffer = null;
835 }
836 StringBuilder save_charBuffer = charBuffer;
837 charBuffer = new StringBuilder(charBuffer.toString());
838 ArrayList<Byte> save_wrapInfo = wrapInfo;
839 wrapInfo = new ArrayList<Byte>(wrapInfo);
840 boolean save_lastCharIsSpace = lastCharIsSpace;
841 int save_forcedBreakCount = forcedBreakCount;
842 forcedBreakCount = 0;
843 fillRow(Integer.MAX_VALUE, true, false);
844 String remainder = charBuffer.toString();
845 inputStream = save_inputStream;
846 inputBuffer = save_inputBuffer != null ? peekingIterator(save_inputBuffer.iterator()) : null;
847 charBuffer = save_charBuffer;
848 wrapInfo = save_wrapInfo;
849 lastCharIsSpace = save_lastCharIsSpace;
850 forcedBreakCount = save_forcedBreakCount;
851 return remainder;
852 }
853
854
855
856
857 public int countRemaining() {
858 return getTranslatedRemainder().length();
859 }
860
861
862
863
864 public BrailleTranslatorResult copy() {
865 return (BrailleTranslatorResult)clone();
866 }
867
868 @Override
869
870 public Object clone() {
871 LineIterator clone; {
872 try {
873 clone = (LineIterator)super.clone();
874 } catch (CloneNotSupportedException e) {
875 throw new InternalError("coding error");
876 }
877 }
878 clone.inputStream = (BrailleStream)inputStream.clone();
879 if (inputBuffer != null) {
880 List<Character> list = ImmutableList.copyOf(inputBuffer);
881 inputBuffer = peekingIterator(list.iterator());
882 clone.inputBuffer = peekingIterator(list.iterator());
883 }
884 clone.charBuffer = new StringBuilder(charBuffer.toString());
885 clone.wrapInfo = new ArrayList<Byte>(wrapInfo);
886 return clone;
887 }
888
889
890 public boolean supportsMetric(String metric) {
891 return METRIC_FORCED_BREAK.equals(metric);
892 }
893
894 public double getMetric(String metric) {
895 if (METRIC_FORCED_BREAK.equals(metric))
896 return forcedBreakCount;
897 else
898 throw new UnsupportedMetricException("Metric not supported: " + metric);
899 }
900
901 private static final BrailleStream emptyBrailleStream = new BrailleStream() {
902 public boolean hasNext() { return false; }
903 public String next(int limit, boolean force, boolean allowHyphens) { throw new NoSuchElementException(); }
904 public Character peek() { throw new NoSuchElementException(); }
905 public String remainder() { throw new NoSuchElementException(); }
906 public boolean hasPrecedingSpace() { return false; }
907 @Override
908 public Object clone() { return this; }
909 };
910 }
911
912
913
914
915 protected static boolean hasPrecedingSpace(String string, int before) {
916 while (before > 0)
917 switch (string.charAt(--before)) {
918 case SPACE:
919 case BLANK:
920 case CR:
921 case LF:
922 case TAB:
923 case LS:
924 return true;
925 case ZWSP:
926 continue;
927 default:
928 return false;
929 }
930 return false;
931 }
932 }
933 }
934 }