Version 4.5.0
[nikiroo-utils.git] / src / be / nikiroo / utils / StringUtils.java
CommitLineData
ec1f3444
NR
1package be.nikiroo.utils;
2
ec1f3444 3import java.io.ByteArrayInputStream;
ec1f3444 4import java.io.IOException;
a359464f
NR
5import java.io.InputStream;
6import java.io.OutputStream;
3f8349b7 7import java.io.UnsupportedEncodingException;
ec1f3444
NR
8import java.security.MessageDigest;
9import java.security.NoSuchAlgorithmException;
10import java.text.Normalizer;
11import java.text.Normalizer.Form;
12import java.text.ParseException;
13import java.text.SimpleDateFormat;
dc22eb95
NR
14import java.util.AbstractMap;
15import java.util.ArrayList;
ec1f3444 16import java.util.Date;
cc3e7291 17import java.util.List;
dc22eb95 18import java.util.Map.Entry;
db31c358 19import java.util.Scanner;
ec1f3444
NR
20import java.util.regex.Pattern;
21
ec1f3444
NR
22import org.unbescape.html.HtmlEscape;
23import org.unbescape.html.HtmlEscapeLevel;
24import org.unbescape.html.HtmlEscapeType;
25
26/**
27 * This class offer some utilities based around {@link String}s.
28 *
29 * @author niki
30 */
31public class StringUtils {
32 /**
33 * This enum type will decide the alignment of a {@link String} when padding
cc3e7291
NR
34 * or justification is applied (if there is enough horizontal space for it
35 * to be aligned).
ec1f3444
NR
36 */
37 public enum Alignment {
38 /** Aligned at left. */
cc3e7291 39 LEFT,
ec1f3444 40 /** Centered. */
cc3e7291 41 CENTER,
ec1f3444 42 /** Aligned at right. */
cc3e7291
NR
43 RIGHT,
44 /** Full justified (to both left and right). */
45 JUSTIFY,
46
47 // Old Deprecated values:
48
49 /** DEPRECATED: please use LEFT. */
50 @Deprecated
51 Beginning,
52 /** DEPRECATED: please use CENTER. */
53 @Deprecated
54 Center,
55 /** DEPRECATED: please use RIGHT. */
56 @Deprecated
57 End;
58
59 /**
60 * Return the non-deprecated version of this enum if needed (or return
61 * self if not).
62 *
63 * @return the non-deprecated value
64 */
65 Alignment undeprecate() {
66 if (this == Beginning)
67 return LEFT;
68 if (this == Center)
69 return CENTER;
70 if (this == End)
71 return RIGHT;
72 return this;
73 }
ec1f3444
NR
74 }
75
e8aa5bf9 76 static private Pattern marks = getMarks();
ec1f3444
NR
77
78 /**
79 * Fix the size of the given {@link String} either with space-padding or by
80 * shortening it.
81 *
82 * @param text
83 * the {@link String} to fix
84 * @param width
85 * the size of the resulting {@link String} or -1 for a noop
86 *
87 * @return the resulting {@link String} of size <i>size</i>
88 */
89 static public String padString(String text, int width) {
451f434b 90 return padString(text, width, true, null);
ec1f3444
NR
91 }
92
93 /**
94 * Fix the size of the given {@link String} either with space-padding or by
95 * optionally shortening it.
96 *
97 * @param text
98 * the {@link String} to fix
99 * @param width
100 * the size of the resulting {@link String} if the text fits or
101 * if cut is TRUE or -1 for a noop
102 * @param cut
103 * cut the {@link String} shorter if needed
104 * @param align
105 * align the {@link String} in this position if we have enough
451f434b 106 * space (default is Alignment.Beginning)
ec1f3444
NR
107 *
108 * @return the resulting {@link String} of size <i>size</i> minimum
109 */
110 static public String padString(String text, int width, boolean cut,
111 Alignment align) {
112
451f434b 113 if (align == null) {
cc3e7291 114 align = Alignment.LEFT;
451f434b
NR
115 }
116
cc3e7291
NR
117 align = align.undeprecate();
118
ec1f3444
NR
119 if (width >= 0) {
120 if (text == null)
121 text = "";
122
123 int diff = width - text.length();
124
125 if (diff < 0) {
126 if (cut)
127 text = text.substring(0, width);
128 } else if (diff > 0) {
cc3e7291
NR
129 if (diff < 2 && align != Alignment.RIGHT)
130 align = Alignment.LEFT;
ec1f3444
NR
131
132 switch (align) {
cc3e7291 133 case RIGHT:
ec1f3444
NR
134 text = new String(new char[diff]).replace('\0', ' ') + text;
135 break;
cc3e7291 136 case CENTER:
ec1f3444
NR
137 int pad1 = (diff) / 2;
138 int pad2 = (diff + 1) / 2;
139 text = new String(new char[pad1]).replace('\0', ' ') + text
140 + new String(new char[pad2]).replace('\0', ' ');
141 break;
cc3e7291
NR
142 case LEFT:
143 default:
144 text = text + new String(new char[diff]).replace('\0', ' ');
145 break;
ec1f3444
NR
146 }
147 }
148 }
149
150 return text;
151 }
152
cc3e7291
NR
153 /**
154 * Justify a text into width-sized (at the maximum) lines.
155 *
156 * @param text
157 * the {@link String} to justify
158 * @param width
159 * the maximum size of the resulting lines
160 *
161 * @return a list of justified text lines
162 */
163 static public List<String> justifyText(String text, int width) {
164 return justifyText(text, width, null);
165 }
166
167 /**
168 * Justify a text into width-sized (at the maximum) lines.
169 *
170 * @param text
171 * the {@link String} to justify
172 * @param width
173 * the maximum size of the resulting lines
174 * @param align
175 * align the lines in this position (default is
176 * Alignment.Beginning)
177 *
178 * @return a list of justified text lines
179 */
180 static public List<String> justifyText(String text, int width,
181 Alignment align) {
182 if (align == null) {
183 align = Alignment.LEFT;
184 }
185
186 align = align.undeprecate();
187
188 switch (align) {
189 case CENTER:
190 return StringJustifier.center(text, width);
191 case RIGHT:
192 return StringJustifier.right(text, width);
193 case JUSTIFY:
194 return StringJustifier.full(text, width);
195 case LEFT:
196 default:
197 return StringJustifier.left(text, width);
198 }
199 }
200
dc22eb95
NR
201 /**
202 * Justify a text into width-sized (at the maximum) lines.
203 *
204 * @param text
205 * the {@link String} to justify
206 * @param width
207 * the maximum size of the resulting lines
208 *
209 * @return a list of justified text lines
210 */
211 static public List<String> justifyText(List<String> text, int width) {
212 return justifyText(text, width, null);
213 }
214
215 /**
216 * Justify a text into width-sized (at the maximum) lines.
217 *
218 * @param text
219 * the {@link String} to justify
220 * @param width
221 * the maximum size of the resulting lines
222 * @param align
223 * align the lines in this position (default is
224 * Alignment.Beginning)
225 *
226 * @return a list of justified text lines
227 */
228 static public List<String> justifyText(List<String> text, int width,
229 Alignment align) {
230 List<String> result = new ArrayList<String>();
231
232 // Content <-> Bullet spacing (null = no spacing)
233 List<Entry<String, String>> lines = new ArrayList<Entry<String, String>>();
234 StringBuilder previous = null;
235 StringBuilder tmp = new StringBuilder();
236 String previousItemBulletSpacing = null;
237 String itemBulletSpacing = null;
238 for (String inputLine : text) {
239 boolean previousLineComplete = true;
240
241 String current = inputLine.replace("\t", " ");
242 itemBulletSpacing = getItemSpacing(current);
243 boolean bullet = isItemLine(current);
244 if ((previousItemBulletSpacing == null || itemBulletSpacing
245 .length() <= previousItemBulletSpacing.length()) && !bullet) {
246 itemBulletSpacing = null;
247 }
248
249 if (itemBulletSpacing != null) {
250 current = current.trim();
251 if (!current.isEmpty() && bullet) {
252 current = current.substring(1);
253 }
254 current = current.trim();
255 previousLineComplete = bullet;
256 } else {
257 tmp.setLength(0);
258 for (String word : current.split(" ")) {
259 if (word.isEmpty()) {
260 continue;
261 }
262
263 if (tmp.length() > 0) {
264 tmp.append(' ');
265 }
266 tmp.append(word.trim());
267 }
268 current = tmp.toString();
269
270 previousLineComplete = current.isEmpty()
271 || previousItemBulletSpacing != null
c0c091af
NR
272 || (previous != null && isFullLine(previous))
273 || isHrLine(current) || isHrLine(previous);
dc22eb95
NR
274 }
275
276 if (previous == null) {
277 previous = new StringBuilder();
278 } else {
279 if (previousLineComplete) {
280 lines.add(new AbstractMap.SimpleEntry<String, String>(
281 previous.toString(), previousItemBulletSpacing));
282 previous.setLength(0);
283 previousItemBulletSpacing = itemBulletSpacing;
284 } else {
285 previous.append(' ');
286 }
287 }
288
289 previous.append(current);
290
291 }
292
293 if (previous != null) {
294 lines.add(new AbstractMap.SimpleEntry<String, String>(previous
295 .toString(), previousItemBulletSpacing));
296 }
297
298 for (Entry<String, String> line : lines) {
299 String content = line.getKey();
300 String spacing = line.getValue();
301
302 String bullet = "- ";
303 if (spacing == null) {
304 bullet = "";
305 spacing = "";
306 }
307
308 if (spacing.length() > width + 3) {
309 spacing = "";
310 }
311
312 for (String subline : StringUtils.justifyText(content, width
313 - (spacing.length() + bullet.length()), align)) {
314 result.add(spacing + bullet + subline);
315 if (!bullet.isEmpty()) {
316 bullet = " ";
317 }
318 }
319 }
320
321 return result;
322 }
323
ec1f3444
NR
324 /**
325 * Sanitise the given input to make it more Terminal-friendly by removing
326 * combining characters.
327 *
328 * @param input
329 * the input to sanitise
330 * @param allowUnicode
331 * allow Unicode or only allow ASCII Latin characters
332 *
333 * @return the sanitised {@link String}
334 */
335 static public String sanitize(String input, boolean allowUnicode) {
336 return sanitize(input, allowUnicode, !allowUnicode);
337 }
338
339 /**
340 * Sanitise the given input to make it more Terminal-friendly by removing
341 * combining characters.
342 *
343 * @param input
344 * the input to sanitise
345 * @param allowUnicode
346 * allow Unicode or only allow ASCII Latin characters
347 * @param removeAllAccents
348 * TRUE to replace all accentuated characters by their non
349 * accentuated counter-parts
350 *
351 * @return the sanitised {@link String}
352 */
353 static public String sanitize(String input, boolean allowUnicode,
354 boolean removeAllAccents) {
355
356 if (removeAllAccents) {
357 input = Normalizer.normalize(input, Form.NFKD);
e8aa5bf9
NR
358 if (marks != null) {
359 input = marks.matcher(input).replaceAll("");
360 }
ec1f3444
NR
361 }
362
363 input = Normalizer.normalize(input, Form.NFKC);
364
365 if (!allowUnicode) {
366 StringBuilder builder = new StringBuilder();
367 for (int index = 0; index < input.length(); index++) {
368 char car = input.charAt(index);
369 // displayable chars in ASCII are in the range 32<->255,
370 // except DEL (127)
371 if (car >= 32 && car <= 255 && car != 127) {
372 builder.append(car);
373 }
374 }
375 input = builder.toString();
376 }
377
378 return input;
379 }
380
381 /**
451f434b
NR
382 * Convert between the time in milliseconds to a {@link String} in a "fixed"
383 * way (to exchange data over the wire, for instance).
384 * <p>
385 * Precise to the second.
ec1f3444
NR
386 *
387 * @param time
451f434b
NR
388 * the specified number of milliseconds since the standard base
389 * time known as "the epoch", namely January 1, 1970, 00:00:00
390 * GMT
ec1f3444
NR
391 *
392 * @return the time as a {@link String}
393 */
394 static public String fromTime(long time) {
395 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
396 return sdf.format(new Date(time));
397 }
398
399 /**
451f434b 400 * Convert between the time as a {@link String} to milliseconds in a "fixed"
ec1f3444 401 * way (to exchange data over the wire, for instance).
451f434b
NR
402 * <p>
403 * Precise to the second.
ec1f3444 404 *
db31c358 405 * @param displayTime
ec1f3444
NR
406 * the time as a {@link String}
407 *
451f434b 408 * @return the number of milliseconds since the standard base time known as
e8aa5bf9
NR
409 * "the epoch", namely January 1, 1970, 00:00:00 GMT, or -1 in case
410 * of error
411 *
412 * @throws ParseException
413 * in case of parse error
ec1f3444 414 */
e8aa5bf9 415 static public long toTime(String displayTime) throws ParseException {
ec1f3444 416 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
e8aa5bf9 417 return sdf.parse(displayTime).getTime();
ec1f3444
NR
418 }
419
ec1f3444
NR
420 /**
421 * Return a hash of the given {@link String}.
422 *
423 * @param input
424 * the input data
425 *
426 * @return the hash
427 */
b771aed5 428 static public String getMd5Hash(String input) {
ec1f3444
NR
429 try {
430 MessageDigest md = MessageDigest.getInstance("MD5");
3f8349b7 431 md.update(input.getBytes("UTF-8"));
ec1f3444
NR
432 byte byteData[] = md.digest();
433
434 StringBuffer hexString = new StringBuffer();
435 for (int i = 0; i < byteData.length; i++) {
436 String hex = Integer.toHexString(0xff & byteData[i]);
437 if (hex.length() == 1)
438 hexString.append('0');
439 hexString.append(hex);
440 }
441
442 return hexString.toString();
443 } catch (NoSuchAlgorithmException e) {
444 return input;
3f8349b7
NR
445 } catch (UnsupportedEncodingException e) {
446 return input;
ec1f3444
NR
447 }
448 }
449
ec1f3444
NR
450 /**
451 * Remove the HTML content from the given input, and un-html-ize the rest.
452 *
453 * @param html
454 * the HTML-encoded content
455 *
456 * @return the HTML-free equivalent content
457 */
458 public static String unhtml(String html) {
459 StringBuilder builder = new StringBuilder();
460
461 int inTag = 0;
462 for (char car : html.toCharArray()) {
463 if (car == '<') {
464 inTag++;
465 } else if (car == '>') {
466 inTag--;
467 } else if (inTag <= 0) {
468 builder.append(car);
469 }
470 }
471
7ee9568b
NR
472 char nbsp = ' '; // non-breakable space (a special char)
473 char space = ' ';
474 return HtmlEscape.unescapeHtml(builder.toString()).replace(nbsp, space);
ec1f3444
NR
475 }
476
477 /**
478 * Escape the given {@link String} so it can be used in XML, as content.
479 *
480 * @param input
481 * the input {@link String}
482 *
483 * @return the escaped {@link String}
484 */
485 public static String xmlEscape(String input) {
486 if (input == null) {
487 return "";
488 }
489
490 return HtmlEscape.escapeHtml(input,
491 HtmlEscapeType.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA,
492 HtmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT);
493 }
494
495 /**
496 * Escape the given {@link String} so it can be used in XML, as text content
497 * inside double-quotes.
498 *
499 * @param input
500 * the input {@link String}
501 *
502 * @return the escaped {@link String}
503 */
504 public static String xmlEscapeQuote(String input) {
505 if (input == null) {
506 return "";
507 }
508
509 return HtmlEscape.escapeHtml(input,
510 HtmlEscapeType.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA,
511 HtmlEscapeLevel.LEVEL_1_ONLY_MARKUP_SIGNIFICANT);
512 }
db31c358 513
80500544
NR
514 /**
515 * Zip the data and then encode it into Base64.
516 *
bb60bd13
NR
517 * @deprecated use {@link StringUtils#base64(byte[], boolean)} with the
518 * correct parameter instead
519 *
80500544
NR
520 * @param data
521 * the data
522 *
523 * @return the Base64 zipped version
524 */
bb60bd13 525 @Deprecated
db31c358
NR
526 public static String zip64(String data) {
527 try {
a359464f 528 return Base64.encodeBytes(data.getBytes("UTF-8"), Base64.GZIP);
db31c358
NR
529 } catch (IOException e) {
530 e.printStackTrace();
531 return null;
532 }
533 }
534
80500544
NR
535 /**
536 * Unconvert from Base64 then unzip the content.
537 *
bb60bd13
NR
538 * @deprecated use {@link StringUtils#unbase64s(String, boolean)} with the
539 * correct parameter instead
540 *
80500544
NR
541 * @param data
542 * the data in Base64 format
543 *
544 * @return the raw data
545 *
546 * @throws IOException
547 * in case of I/O error
548 */
bb60bd13 549 @Deprecated
db31c358
NR
550 public static String unzip64(String data) throws IOException {
551 ByteArrayInputStream in = new ByteArrayInputStream(Base64.decode(data,
552 Base64.GZIP));
553
554 Scanner scan = new Scanner(in);
555 scan.useDelimiter("\\A");
556 try {
557 return scan.next();
558 } finally {
559 scan.close();
560 }
561 }
e8aa5bf9 562
a359464f
NR
563 /**
564 * Convert the given data to Base64 format.
565 *
566 * @param data
567 * the data to convert
568 * @param zip
569 * TRUE to also compress the data in GZIP format; remember that
570 * compressed and not-compressed content are different; you need
571 * to know which is which when decoding
572 *
573 * @return the Base64 {@link String} representation of the data
574 *
575 * @throws IOException
576 * in case of I/O errors
577 */
578 public static String base64(String data, boolean zip) throws IOException {
579 return base64(data.getBytes("UTF-8"), zip);
580 }
581
bb60bd13
NR
582 /**
583 * Convert the given data to Base64 format.
584 *
585 * @param data
586 * the data to convert
587 * @param zip
588 * TRUE to also compress the data in GZIP format; remember that
589 * compressed and not-compressed content are different; you need
590 * to know which is which when decoding
591 *
592 * @return the Base64 {@link String} representation of the data
593 *
594 * @throws IOException
595 * in case of I/O errors
596 */
597 public static String base64(byte[] data, boolean zip) throws IOException {
598 return Base64.encodeBytes(data, zip ? Base64.GZIP : Base64.NO_OPTIONS);
599 }
600
601 /**
a359464f
NR
602 * Convert the given data to Base64 format.
603 *
604 * @param data
605 * the data to convert
606 * @param zip
607 * TRUE to also uncompress the data from a GZIP format; take care
608 * about this flag, as it could easily cause errors in the
609 * returned content or an {@link IOException}
610 * @param breakLines
611 * TRUE to break lines on every 76th character
612 *
613 * @return the Base64 {@link String} representation of the data
614 *
615 * @throws IOException
616 * in case of I/O errors
617 */
618 public static OutputStream base64(OutputStream data, boolean zip,
619 boolean breakLines) throws IOException {
620 OutputStream out = new Base64.OutputStream(data,
621 breakLines ? Base64.DO_BREAK_LINES & Base64.ENCODE
622 : Base64.ENCODE);
623
624 if (zip) {
625 out = new java.util.zip.GZIPOutputStream(out);
626 }
627
628 return out;
629 }
630
631 /**
632 * Convert the given data to Base64 format.
633 *
634 * @param data
635 * the data to convert
636 * @param zip
637 * TRUE to also uncompress the data from a GZIP format; take care
638 * about this flag, as it could easily cause errors in the
639 * returned content or an {@link IOException}
640 * @param breakLines
641 * TRUE to break lines on every 76th character
642 *
643 * @return the Base64 {@link String} representation of the data
644 *
645 * @throws IOException
646 * in case of I/O errors
647 */
648 public static InputStream base64(InputStream data, boolean zip,
649 boolean breakLines) throws IOException {
650 if (zip) {
651 data = new java.util.zip.GZIPInputStream(data);
652 }
653
654 return new Base64.InputStream(data, breakLines ? Base64.DO_BREAK_LINES
655 & Base64.ENCODE : Base64.ENCODE);
656 }
657
658 /**
659 * Unconvert the given data from Base64 format back to a raw array of bytes.
bb60bd13
NR
660 *
661 * @param data
662 * the data to unconvert
663 * @param zip
664 * TRUE to also uncompress the data from a GZIP format; take care
665 * about this flag, as it could easily cause errors in the
666 * returned content or an {@link IOException}
667 *
668 * @return the raw data represented by the given Base64 {@link String},
669 * optionally compressed with GZIP
670 *
671 * @throws IOException
672 * in case of I/O errors
673 */
674 public static byte[] unbase64(String data, boolean zip) throws IOException {
675 return Base64.decode(data, zip ? Base64.GZIP : Base64.NO_OPTIONS);
676 }
677
a359464f
NR
678 /**
679 * Unconvert the given data from Base64 format back to a raw array of bytes.
680 *
681 * @param data
682 * the data to unconvert
683 * @param zip
684 * TRUE to also uncompress the data from a GZIP format; take care
685 * about this flag, as it could easily cause errors in the
686 * returned content or an {@link IOException}
687 * @param breakLines
688 * TRUE to break lines on every 76th character
689 *
690 * @return the raw data represented by the given Base64 {@link String}
691 *
692 * @throws IOException
693 * in case of I/O errors
694 */
695 public static OutputStream unbase64(OutputStream data, boolean zip,
696 boolean breakLines) throws IOException {
697 OutputStream out = new Base64.OutputStream(data,
698 breakLines ? Base64.DO_BREAK_LINES & Base64.ENCODE
699 : Base64.ENCODE);
700
701 if (zip) {
702 out = new java.util.zip.GZIPOutputStream(out);
703 }
704
705 return out;
706 }
707
708 /**
709 * Unconvert the given data from Base64 format back to a raw array of bytes.
710 *
711 * @param data
712 * the data to unconvert
713 * @param zip
714 * TRUE to also uncompress the data from a GZIP format; take care
715 * about this flag, as it could easily cause errors in the
716 * returned content or an {@link IOException}
717 * @param breakLines
718 * TRUE to break lines on every 76th character
719 *
720 * @return the raw data represented by the given Base64 {@link String}
721 *
722 * @throws IOException
723 * in case of I/O errors
724 */
725 public static InputStream unbase64(InputStream data, boolean zip,
726 boolean breakLines) throws IOException {
727 if (zip) {
728 data = new java.util.zip.GZIPInputStream(data);
729 }
730
731 return new Base64.InputStream(data, breakLines ? Base64.DO_BREAK_LINES
732 & Base64.ENCODE : Base64.ENCODE);
733 }
734
bb60bd13
NR
735 /**
736 * Unonvert the given data from Base64 format back to a {@link String}.
737 *
738 * @param data
739 * the data to unconvert
740 * @param zip
741 * TRUE to also uncompress the data from a GZIP format; take care
742 * about this flag, as it could easily cause errors in the
743 * returned content or an {@link IOException}
744 *
745 * @return the {@link String} represented by the given Base64 {@link String}
746 * , optionally compressed with GZIP
747 *
748 * @throws IOException
749 * in case of I/O errors
750 */
751 public static String unbase64s(String data, boolean zip) throws IOException {
752 ByteArrayInputStream in = new ByteArrayInputStream(unbase64(data, zip));
753
a359464f 754 Scanner scan = new Scanner(in, "UTF-8");
bb60bd13
NR
755 scan.useDelimiter("\\A");
756 try {
757 return scan.next();
758 } finally {
759 scan.close();
760 }
761 }
762
e8aa5bf9
NR
763 /**
764 * The "remove accents" pattern.
765 *
766 * @return the pattern, or NULL if a problem happens
767 */
768 private static Pattern getMarks() {
769 try {
770 return Pattern
771 .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
772 } catch (Exception e) {
773 // Can fail on Android...
774 return null;
775 }
776 }
dc22eb95 777
bb60bd13 778 //
dc22eb95 779 // justify List<String> related:
bb60bd13 780 //
dc22eb95 781
bb60bd13
NR
782 /**
783 * Check if this line ends as a complete line (ends with a "." or similar).
784 * <p>
785 * Note that we consider an empty line as full, and a line ending with
786 * spaces as not complete.
787 *
788 * @param line
789 * the line to check
790 *
791 * @return TRUE if it does
792 */
dc22eb95 793 static private boolean isFullLine(StringBuilder line) {
bb60bd13
NR
794 if (line.length() == 0) {
795 return true;
796 }
797
798 char lastCar = line.charAt(line.length() - 1);
799 switch (lastCar) {
800 case '.': // points
801 case '?':
802 case '!':
803
804 case '\'': // quotes
805 case '‘':
806 case '’':
807
808 case '"': // double quotes
809 case '”':
810 case '“':
811 case '»':
812 case '«':
813 return true;
814 default:
815 return false;
816 }
dc22eb95
NR
817 }
818
bb60bd13
NR
819 /**
820 * Check if this line represent an item in a list or description (i.e.,
821 * check that the first non-space char is "-").
822 *
823 * @param line
824 * the line to check
825 *
826 * @return TRUE if it is
827 */
dc22eb95
NR
828 static private boolean isItemLine(String line) {
829 String spacing = getItemSpacing(line);
c0c091af
NR
830 return spacing != null && !spacing.isEmpty()
831 && line.charAt(spacing.length()) == '-';
dc22eb95
NR
832 }
833
bb60bd13
NR
834 /**
835 * Return all the spaces that start this line (or Empty if none).
836 *
837 * @param line
838 * the line to get the starting spaces from
839 *
840 * @return the left spacing
841 */
dc22eb95
NR
842 static private String getItemSpacing(String line) {
843 int i;
844 for (i = 0; i < line.length(); i++) {
845 if (line.charAt(i) != ' ') {
846 return line.substring(0, i);
847 }
848 }
849
850 return "";
851 }
c0c091af 852
bb60bd13
NR
853 /**
854 * This line is an horizontal spacer line.
855 *
856 * @param line
857 * the line to test
858 *
859 * @return TRUE if it is
860 */
c0c091af
NR
861 static private boolean isHrLine(CharSequence line) {
862 int count = 0;
863 if (line != null) {
864 for (int i = 0; i < line.length(); i++) {
865 char car = line.charAt(i);
866 if (car == ' ' || car == '\t' || car == '*' || car == '-'
867 || car == '_' || car == '~' || car == '=' || car == '/'
868 || car == '\\') {
869 count++;
870 } else {
871 return false;
872 }
873 }
874 }
875
876 return count > 2;
877 }
ec1f3444 878}