Add progress reporting on GUI
[fanfix.git] / src / be / nikiroo / fanfix / supported / BasicSupport.java
1 package be.nikiroo.fanfix.supported;
2
3 import java.awt.image.BufferedImage;
4 import java.io.ByteArrayInputStream;
5 import java.io.File;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.net.MalformedURLException;
9 import java.net.URL;
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Map.Entry;
15 import java.util.Scanner;
16
17 import be.nikiroo.fanfix.Instance;
18 import be.nikiroo.fanfix.bundles.Config;
19 import be.nikiroo.fanfix.bundles.StringId;
20 import be.nikiroo.fanfix.data.Chapter;
21 import be.nikiroo.fanfix.data.MetaData;
22 import be.nikiroo.fanfix.data.Paragraph;
23 import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
24 import be.nikiroo.fanfix.data.Story;
25 import be.nikiroo.utils.IOUtils;
26 import be.nikiroo.utils.Progress;
27 import be.nikiroo.utils.StringUtils;
28
29 /**
30 * This class is the base class used by the other support classes. It can be
31 * used outside of this package, and have static method that you can use to get
32 * access to the correct support class.
33 * <p>
34 * It will be used with 'resources' (usually web pages or files).
35 *
36 * @author niki
37 */
38 public abstract class BasicSupport {
39 /**
40 * The supported input types for which we can get a {@link BasicSupport}
41 * object.
42 *
43 * @author niki
44 */
45 public enum SupportType {
46 /** EPUB files created with this program */
47 EPUB,
48 /** Pure text file with some rules */
49 TEXT,
50 /** TEXT but with associated .info file */
51 INFO_TEXT,
52 /** My Little Pony fanfictions */
53 FIMFICTION,
54 /** Fanfictions from a lot of different universes */
55 FANFICTION,
56 /** Website with lots of Mangas */
57 MANGAFOX,
58 /** Furry website with comics support */
59 E621,
60 /** CBZ files */
61 CBZ;
62
63 /**
64 * A description of this support type (more information than the
65 * {@link BasicSupport#getSourceName()}).
66 *
67 * @return the description
68 */
69 public String getDesc() {
70 String desc = Instance.getTrans().getStringX(StringId.INPUT_DESC,
71 this.name());
72
73 if (desc == null) {
74 desc = Instance.getTrans().getString(StringId.INPUT_DESC, this);
75 }
76
77 return desc;
78 }
79
80 /**
81 * The name of this support type (a short version).
82 *
83 * @return the name
84 */
85 public String getSourceName() {
86 BasicSupport support = BasicSupport.getSupport(this);
87 if (support != null) {
88 return support.getSourceName();
89 }
90
91 return null;
92 }
93
94 @Override
95 public String toString() {
96 return super.toString().toLowerCase();
97 }
98
99 /**
100 * Call {@link SupportType#valueOf(String.toUpperCase())}.
101 *
102 * @param typeName
103 * the possible type name
104 *
105 * @return NULL or the type
106 */
107 public static SupportType valueOfUC(String typeName) {
108 return SupportType.valueOf(typeName == null ? null : typeName
109 .toUpperCase());
110 }
111
112 /**
113 * Call {@link SupportType#valueOf(String.toUpperCase())} but return
114 * NULL for NULL instead of raising exception.
115 *
116 * @param typeName
117 * the possible type name
118 *
119 * @return NULL or the type
120 */
121 public static SupportType valueOfNullOkUC(String typeName) {
122 if (typeName == null) {
123 return null;
124 }
125
126 return SupportType.valueOfUC(typeName);
127 }
128
129 /**
130 * Call {@link SupportType#valueOf(String.toUpperCase())} but return
131 * NULL in case of error instead of raising an exception.
132 *
133 * @param typeName
134 * the possible type name
135 *
136 * @return NULL or the type
137 */
138 public static SupportType valueOfAllOkUC(String typeName) {
139 try {
140 return SupportType.valueOfUC(typeName);
141 } catch (Exception e) {
142 return null;
143 }
144 }
145 }
146
147 private InputStream in;
148 private SupportType type;
149 private URL currentReferer; // with on 'r', as in 'HTTP'...
150
151 // quote chars
152 private char openQuote = Instance.getTrans().getChar(
153 StringId.OPEN_SINGLE_QUOTE);
154 private char closeQuote = Instance.getTrans().getChar(
155 StringId.CLOSE_SINGLE_QUOTE);
156 private char openDoubleQuote = Instance.getTrans().getChar(
157 StringId.OPEN_DOUBLE_QUOTE);
158 private char closeDoubleQuote = Instance.getTrans().getChar(
159 StringId.CLOSE_DOUBLE_QUOTE);
160
161 /**
162 * The name of this support class.
163 *
164 * @return the name
165 */
166 protected abstract String getSourceName();
167
168 /**
169 * Check if the given resource is supported by this {@link BasicSupport}.
170 *
171 * @param url
172 * the resource to check for
173 *
174 * @return TRUE if it is
175 */
176 protected abstract boolean supports(URL url);
177
178 /**
179 * Return TRUE if the support will return HTML encoded content values for
180 * the chapters content.
181 *
182 * @return TRUE for HTML
183 */
184 protected abstract boolean isHtml();
185
186 protected abstract MetaData getMeta(URL source, InputStream in)
187 throws IOException;
188
189 /**
190 * Return the story description.
191 *
192 * @param source
193 * the source of the story
194 * @param in
195 * the input (the main resource)
196 *
197 * @return the description
198 *
199 * @throws IOException
200 * in case of I/O error
201 */
202 protected abstract String getDesc(URL source, InputStream in)
203 throws IOException;
204
205 /**
206 * Return the list of chapters (name and resource).
207 *
208 * @param source
209 * the source of the story
210 * @param in
211 * the input (the main resource)
212 *
213 * @return the chapters
214 *
215 * @throws IOException
216 * in case of I/O error
217 */
218 protected abstract List<Entry<String, URL>> getChapters(URL source,
219 InputStream in) throws IOException;
220
221 /**
222 * Return the content of the chapter (possibly HTML encoded, if
223 * {@link BasicSupport#isHtml()} is TRUE).
224 *
225 * @param source
226 * the source of the story
227 * @param in
228 * the input (the main resource)
229 * @param number
230 * the chapter number
231 *
232 * @return the content
233 *
234 * @throws IOException
235 * in case of I/O error
236 */
237 protected abstract String getChapterContent(URL source, InputStream in,
238 int number) throws IOException;
239
240 /**
241 * Return the list of cookies (values included) that must be used to
242 * correctly fetch the resources.
243 * <p>
244 * You are expected to call the super method implementation if you override
245 * it.
246 *
247 * @return the cookies
248 */
249 public Map<String, String> getCookies() {
250 return new HashMap<String, String>();
251 }
252
253 /**
254 * Process the given story resource into a partially filled {@link Story}
255 * object containing the name and metadata, except for the description.
256 *
257 * @param url
258 * the story resource
259 *
260 * @return the {@link Story}
261 *
262 * @throws IOException
263 * in case of I/O error
264 */
265 public Story processMeta(URL url) throws IOException {
266 return processMeta(url, true, false);
267 }
268
269 /**
270 * Process the given story resource into a partially filled {@link Story}
271 * object containing the name and metadata.
272 *
273 * @param url
274 * the story resource
275 *
276 * @param close
277 * close "this" and "in" when done
278 *
279 * @return the {@link Story}
280 *
281 * @throws IOException
282 * in case of I/O error
283 */
284 protected Story processMeta(URL url, boolean close, boolean getDesc)
285 throws IOException {
286 in = Instance.getCache().open(url, this, false);
287 if (in == null) {
288 return null;
289 }
290
291 try {
292 preprocess(url, getInput());
293
294 Story story = new Story();
295 MetaData meta = getMeta(url, getInput());
296 story.setMeta(meta);
297
298 if (meta != null && meta.getCover() == null) {
299 meta.setCover(getDefaultCover(meta.getSubject()));
300 }
301
302 if (getDesc) {
303 String descChapterName = Instance.getTrans().getString(
304 StringId.DESCRIPTION);
305 story.getMeta().setResume(
306 makeChapter(url, 0, descChapterName,
307 getDesc(url, getInput())));
308 }
309
310 return story;
311 } finally {
312 if (close) {
313 try {
314 close();
315 } catch (IOException e) {
316 Instance.syserr(e);
317 }
318
319 if (in != null) {
320 in.close();
321 }
322 }
323 }
324 }
325
326 /**
327 * Process the given story resource into a fully filled {@link Story}
328 * object.
329 *
330 * @param url
331 * the story resource
332 * @param pg
333 * the optional progress reporter
334 *
335 * @return the {@link Story}
336 *
337 * @throws IOException
338 * in case of I/O error
339 */
340 public Story process(URL url, Progress pg) throws IOException {
341 if (pg == null) {
342 pg = new Progress();
343 } else {
344 pg.setMinMax(0, 100);
345 }
346
347 setCurrentReferer(url);
348
349 pg.setProgress(1);
350 try {
351 Story story = processMeta(url, false, true);
352 pg.setProgress(10);
353 if (story == null) {
354 pg.setProgress(100);
355 return null;
356 }
357
358 story.setChapters(new ArrayList<Chapter>());
359
360 List<Entry<String, URL>> chapters = getChapters(url, getInput());
361 pg.setProgress(20);
362
363 int i = 1;
364 if (chapters != null) {
365 Progress pgChaps = new Progress(0, chapters.size());
366 pg.addProgress(pgChaps, 80);
367
368 for (Entry<String, URL> chap : chapters) {
369 setCurrentReferer(chap.getValue());
370 InputStream chapIn = Instance.getCache().open(
371 chap.getValue(), this, true);
372 try {
373 story.getChapters().add(
374 makeChapter(url, i, chap.getKey(),
375 getChapterContent(url, chapIn, i)));
376 } finally {
377 chapIn.close();
378 }
379
380 pgChaps.setProgress(i++);
381 }
382 } else {
383 pg.setProgress(100);
384 }
385
386 return story;
387
388 } finally {
389 try {
390 close();
391 } catch (IOException e) {
392 Instance.syserr(e);
393 }
394
395 if (in != null) {
396 in.close();
397 }
398
399 currentReferer = null;
400 }
401 }
402
403 /**
404 * The support type.$
405 *
406 * @return the type
407 */
408 public SupportType getType() {
409 return type;
410 }
411
412 /**
413 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
414 * the current {@link URL} we work on.
415 *
416 * @return the referer
417 */
418 public URL getCurrentReferer() {
419 return currentReferer;
420 }
421
422 /**
423 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
424 * the current {@link URL} we work on.
425 *
426 * @param currentReferer
427 * the new referer
428 */
429 protected void setCurrentReferer(URL currentReferer) {
430 this.currentReferer = currentReferer;
431 }
432
433 /**
434 * The support type.
435 *
436 * @param type
437 * the new type
438 *
439 * @return this
440 */
441 protected BasicSupport setType(SupportType type) {
442 this.type = type;
443 return this;
444 }
445
446 /**
447 * Prepare the support if needed before processing.
448 *
449 * @param source
450 * the source of the story
451 * @param in
452 * the input (the main resource)
453 *
454 * @throws IOException
455 * on I/O error
456 */
457 protected void preprocess(URL source, InputStream in) throws IOException {
458 }
459
460 /**
461 * Now that we have processed the {@link Story}, close the resources if any.
462 *
463 * @throws IOException
464 * on I/O error
465 */
466 protected void close() throws IOException {
467 }
468
469 /**
470 * Create a {@link Chapter} object from the given information, formatting
471 * the content as it should be.
472 *
473 * @param number
474 * the chapter number
475 * @param name
476 * the chapter name
477 * @param content
478 * the chapter content
479 *
480 * @return the {@link Chapter}
481 *
482 * @throws IOException
483 * in case of I/O error
484 */
485 protected Chapter makeChapter(URL source, int number, String name,
486 String content) throws IOException {
487 // Chapter name: process it correctly, then remove the possible
488 // redundant "Chapter x: " in front of it
489 String chapterName = processPara(name).getContent().trim();
490 for (String lang : Instance.getConfig().getString(Config.CHAPTER)
491 .split(",")) {
492 String chapterWord = Instance.getConfig().getStringX(
493 Config.CHAPTER, lang);
494 if (chapterName.startsWith(chapterWord)) {
495 chapterName = chapterName.substring(chapterWord.length())
496 .trim();
497 break;
498 }
499 }
500
501 if (chapterName.startsWith(Integer.toString(number))) {
502 chapterName = chapterName.substring(
503 Integer.toString(number).length()).trim();
504 }
505
506 if (chapterName.startsWith(":")) {
507 chapterName = chapterName.substring(1).trim();
508 }
509 //
510
511 Chapter chap = new Chapter(number, chapterName);
512
513 if (content == null) {
514 return chap;
515 }
516
517 if (isHtml()) {
518 // Special <HR> processing:
519 content = content.replaceAll("(<hr [^>]*>)|(<hr/>)|(<hr>)",
520 "\n* * *\n");
521 }
522
523 InputStream in = new ByteArrayInputStream(content.getBytes("UTF-8"));
524 try {
525 @SuppressWarnings("resource")
526 Scanner scan = new Scanner(in, "UTF-8");
527 scan.useDelimiter("(\\n|</p>)"); // \n for test, </p> for html
528
529 List<Paragraph> paras = new ArrayList<Paragraph>();
530 while (scan.hasNext()) {
531 String line = scan.next().trim();
532 boolean image = false;
533 if (line.startsWith("[") && line.endsWith("]")) {
534 URL url = getImageUrl(this, source,
535 line.substring(1, line.length() - 1).trim());
536 if (url != null) {
537 paras.add(new Paragraph(url));
538 image = true;
539 }
540 }
541
542 if (!image) {
543 paras.add(processPara(line));
544 }
545 }
546
547 // Check quotes for "bad" format
548 List<Paragraph> newParas = new ArrayList<Paragraph>();
549 for (Paragraph para : paras) {
550 newParas.addAll(requotify(para));
551 }
552 paras = newParas;
553
554 // Remove double blanks/brks
555 boolean space = false;
556 boolean brk = true;
557 for (int i = 0; i < paras.size(); i++) {
558 Paragraph para = paras.get(i);
559 boolean thisSpace = para.getType() == ParagraphType.BLANK;
560 boolean thisBrk = para.getType() == ParagraphType.BREAK;
561
562 if (space && thisBrk) {
563 paras.remove(i - 1);
564 i--;
565 } else if ((space || brk) && (thisSpace || thisBrk)) {
566 paras.remove(i);
567 i--;
568 }
569
570 space = thisSpace;
571 brk = thisBrk;
572 }
573
574 // Remove blank/brk at start
575 if (paras.size() > 0
576 && (paras.get(0).getType() == ParagraphType.BLANK || paras
577 .get(0).getType() == ParagraphType.BREAK)) {
578 paras.remove(0);
579 }
580
581 // Remove blank/brk at end
582 int last = paras.size() - 1;
583 if (paras.size() > 0
584 && (paras.get(last).getType() == ParagraphType.BLANK || paras
585 .get(last).getType() == ParagraphType.BREAK)) {
586 paras.remove(last);
587 }
588
589 chap.setParagraphs(paras);
590
591 return chap;
592 } finally {
593 in.close();
594 }
595 }
596
597 static BufferedImage getDefaultCover(String subject) {
598 if (subject != null && !subject.isEmpty()
599 && Instance.getCoverDir() != null) {
600 try {
601 File fileCover = new File(Instance.getCoverDir(), subject);
602 return getImage(null, fileCover.toURI().toURL(), subject);
603 } catch (MalformedURLException e) {
604 }
605 }
606
607 return null;
608 }
609
610 /**
611 * Return the list of supported image extensions.
612 *
613 * @return the extensions
614 */
615 static String[] getImageExt(boolean emptyAllowed) {
616 if (emptyAllowed) {
617 return new String[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
618 } else {
619 return new String[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
620 }
621 }
622
623 static BufferedImage getImage(BasicSupport support, URL source, String line) {
624 URL url = getImageUrl(support, source, line);
625 if (url != null) {
626 InputStream in = null;
627 try {
628 in = Instance.getCache().open(url, getSupport(url), true);
629 return IOUtils.toImage(in);
630 } catch (IOException e) {
631 } finally {
632 if (in != null) {
633 try {
634 in.close();
635 } catch (IOException e) {
636 }
637 }
638 }
639 }
640
641 return null;
642 }
643
644 /**
645 * Check if the given resource can be a local image or a remote image, then
646 * refresh the cache with it if it is.
647 *
648 * @param source
649 * the story source
650 * @param line
651 * the resource to check
652 *
653 * @return the image URL if found, or NULL
654 *
655 */
656 static URL getImageUrl(BasicSupport support, URL source, String line) {
657 URL url = null;
658
659 if (line != null) {
660 // try for files
661 String path = null;
662 if (source != null) {
663 path = new File(source.getFile()).getParent();
664 try {
665 String basePath = new File(new File(path), line.trim())
666 .getAbsolutePath();
667 for (String ext : getImageExt(true)) {
668 if (new File(basePath + ext).exists()) {
669 url = new File(basePath + ext).toURI().toURL();
670 }
671 }
672 } catch (Exception e) {
673 // Nothing to do here
674 }
675 }
676
677 if (url == null) {
678 // try for URLs
679 try {
680 for (String ext : getImageExt(true)) {
681 if (Instance.getCache().check(new URL(line + ext))) {
682 url = new URL(line + ext);
683 break;
684 }
685 }
686
687 // try out of cache
688 if (url == null) {
689 for (String ext : getImageExt(true)) {
690 try {
691 url = new URL(line + ext);
692 Instance.getCache().refresh(url, support, true);
693 break;
694 } catch (IOException e) {
695 // no image with this ext
696 url = null;
697 }
698 }
699 }
700 } catch (MalformedURLException e) {
701 // Not an url
702 }
703 }
704
705 // refresh the cached file
706 if (url != null) {
707 try {
708 Instance.getCache().refresh(url, support, true);
709 } catch (IOException e) {
710 // woops, broken image
711 url = null;
712 }
713 }
714 }
715
716 return url;
717 }
718
719 protected InputStream reset(InputStream in) {
720 try {
721 in.reset();
722 } catch (IOException e) {
723 }
724 return in;
725 }
726
727 /**
728 * Reset then return {@link BasicSupport#in}.
729 *
730 * @return {@link BasicSupport#in}
731 */
732 protected InputStream getInput() {
733 return reset(in);
734 }
735
736 /**
737 * Fix the author name if it is prefixed with some "by" {@link String}.
738 *
739 * @param author
740 * the author with a possible prefix
741 *
742 * @return the author without prefixes
743 */
744 protected String fixAuthor(String author) {
745 if (author != null) {
746 for (String suffix : new String[] { " ", ":" }) {
747 for (String byString : Instance.getConfig()
748 .getString(Config.BYS).split(",")) {
749 byString += suffix;
750 if (author.toUpperCase().startsWith(byString.toUpperCase())) {
751 author = author.substring(byString.length()).trim();
752 }
753 }
754 }
755
756 // Special case (without suffix):
757 if (author.startsWith("©")) {
758 author = author.substring(1);
759 }
760 }
761
762 return author;
763 }
764
765 /**
766 * Check quotes for bad format (i.e., quotes with normal paragraphs inside)
767 * and requotify them (i.e., separate them into QUOTE paragraphs and other
768 * paragraphs (quotes or not)).
769 *
770 * @param para
771 * the paragraph to requotify (not necessaraly a quote)
772 *
773 * @return the correctly (or so we hope) quotified paragraphs
774 */
775 private List<Paragraph> requotify(Paragraph para) {
776 List<Paragraph> newParas = new ArrayList<Paragraph>();
777
778 if (para.getType() == ParagraphType.QUOTE
779 && para.getContent().length() > 2) {
780 String line = para.getContent();
781 boolean singleQ = line.startsWith("" + openQuote);
782 boolean doubleQ = line.startsWith("" + openDoubleQuote);
783
784 // Do not try when more than one quote at a time
785 // (some stories are not easily readable if we do)
786 if (singleQ
787 && line.indexOf(closeQuote, 1) < line
788 .lastIndexOf(closeQuote)) {
789 newParas.add(para);
790 return newParas;
791 }
792 if (doubleQ
793 && line.indexOf(closeDoubleQuote, 1) < line
794 .lastIndexOf(closeDoubleQuote)) {
795 newParas.add(para);
796 return newParas;
797 }
798 //
799
800 if (!singleQ && !doubleQ) {
801 line = openDoubleQuote + line + closeDoubleQuote;
802 newParas.add(new Paragraph(ParagraphType.QUOTE, line));
803 } else {
804 char open = singleQ ? openQuote : openDoubleQuote;
805 char close = singleQ ? closeQuote : closeDoubleQuote;
806
807 int posDot = -1;
808 boolean inQuote = false;
809 int i = 0;
810 for (char car : line.toCharArray()) {
811 if (car == open) {
812 inQuote = true;
813 } else if (car == close) {
814 inQuote = false;
815 } else if (car == '.' && !inQuote) {
816 posDot = i;
817 break;
818 }
819 i++;
820 }
821
822 if (posDot >= 0) {
823 String rest = line.substring(posDot + 1).trim();
824 line = line.substring(0, posDot + 1).trim();
825 newParas.add(new Paragraph(ParagraphType.QUOTE, line));
826 if (!rest.isEmpty()) {
827 newParas.addAll(requotify(processPara(rest)));
828 }
829 } else {
830 newParas.add(para);
831 }
832 }
833 } else {
834 newParas.add(para);
835 }
836
837 return newParas;
838 }
839
840 /**
841 * Process a {@link Paragraph} from a raw line of text.
842 * <p>
843 * Will also fix quotes and HTML encoding if needed.
844 *
845 * @param line
846 * the raw line
847 *
848 * @return the processed {@link Paragraph}
849 */
850 private Paragraph processPara(String line) {
851 line = ifUnhtml(line).trim();
852
853 boolean space = true;
854 boolean brk = true;
855 boolean quote = false;
856 boolean tentativeCloseQuote = false;
857 char prev = '\0';
858 int dashCount = 0;
859
860 StringBuilder builder = new StringBuilder();
861 for (char car : line.toCharArray()) {
862 if (car != '-') {
863 if (dashCount > 0) {
864 // dash, ndash and mdash: - – —
865 // currently: always use mdash
866 builder.append(dashCount == 1 ? '-' : '—');
867 }
868 dashCount = 0;
869 }
870
871 if (tentativeCloseQuote) {
872 tentativeCloseQuote = false;
873 if ((car >= 'a' && car <= 'z') || (car >= 'A' && car <= 'Z')
874 || (car >= '0' && car <= '9')) {
875 builder.append("'");
876 } else {
877 builder.append(closeQuote);
878 }
879 }
880
881 switch (car) {
882 case ' ': // note: unbreakable space
883 case ' ':
884 case '\t':
885 case '\n': // just in case
886 case '\r': // just in case
887 builder.append(' ');
888 break;
889
890 case '\'':
891 if (space || (brk && quote)) {
892 quote = true;
893 builder.append(openQuote);
894 } else if (prev == ' ') {
895 builder.append(openQuote);
896 } else {
897 // it is a quote ("I'm off") or a 'quote' ("This
898 // 'good' restaurant"...)
899 tentativeCloseQuote = true;
900 }
901 break;
902
903 case '"':
904 if (space || (brk && quote)) {
905 quote = true;
906 builder.append(openDoubleQuote);
907 } else if (prev == ' ') {
908 builder.append(openDoubleQuote);
909 } else {
910 builder.append(closeDoubleQuote);
911 }
912 break;
913
914 case '-':
915 if (space) {
916 quote = true;
917 } else {
918 dashCount++;
919 }
920 space = false;
921 break;
922
923 case '*':
924 case '~':
925 case '/':
926 case '\\':
927 case '<':
928 case '>':
929 case '=':
930 case '+':
931 case '_':
932 case '–':
933 case '—':
934 space = false;
935 builder.append(car);
936 break;
937
938 case '‘':
939 case '`':
940 case '‹':
941 case '﹁':
942 case '〈':
943 case '「':
944 if (space || (brk && quote)) {
945 quote = true;
946 builder.append(openQuote);
947 } else {
948 builder.append(openQuote);
949 }
950 space = false;
951 brk = false;
952 break;
953
954 case '’':
955 case '›':
956 case '﹂':
957 case '〉':
958 case '」':
959 space = false;
960 brk = false;
961 builder.append(closeQuote);
962 break;
963
964 case '«':
965 case '“':
966 case '﹃':
967 case '《':
968 case '『':
969 if (space || (brk && quote)) {
970 quote = true;
971 builder.append(openDoubleQuote);
972 } else {
973 builder.append(openDoubleQuote);
974 }
975 space = false;
976 brk = false;
977 break;
978
979 case '»':
980 case '”':
981 case '﹄':
982 case '》':
983 case '』':
984 space = false;
985 brk = false;
986 builder.append(closeDoubleQuote);
987 break;
988
989 default:
990 space = false;
991 brk = false;
992 builder.append(car);
993 break;
994 }
995
996 prev = car;
997 }
998
999 if (tentativeCloseQuote) {
1000 tentativeCloseQuote = false;
1001 builder.append(closeQuote);
1002 }
1003
1004 line = builder.toString().trim();
1005
1006 ParagraphType type = ParagraphType.NORMAL;
1007 if (space) {
1008 type = ParagraphType.BLANK;
1009 } else if (brk) {
1010 type = ParagraphType.BREAK;
1011 } else if (quote) {
1012 type = ParagraphType.QUOTE;
1013 }
1014
1015 return new Paragraph(type, line);
1016 }
1017
1018 /**
1019 * Remove the HTML from the inpit <b>if</b> {@link BasicSupport#isHtml()} is
1020 * true.
1021 *
1022 * @param input
1023 * the input
1024 *
1025 * @return the no html version if needed
1026 */
1027 private String ifUnhtml(String input) {
1028 if (isHtml() && input != null) {
1029 return StringUtils.unhtml(input);
1030 }
1031
1032 return input;
1033 }
1034
1035 /**
1036 * Return a {@link BasicSupport} implementation supporting the given
1037 * resource if possible.
1038 *
1039 * @param url
1040 * the story resource
1041 *
1042 * @return an implementation that supports it, or NULL
1043 */
1044 public static BasicSupport getSupport(URL url) {
1045 if (url == null) {
1046 return null;
1047 }
1048
1049 // TEXT and INFO_TEXT always support files (not URLs though)
1050 for (SupportType type : SupportType.values()) {
1051 if (type != SupportType.TEXT && type != SupportType.INFO_TEXT) {
1052 BasicSupport support = getSupport(type);
1053 if (support != null && support.supports(url)) {
1054 return support;
1055 }
1056 }
1057 }
1058
1059 for (SupportType type : new SupportType[] { SupportType.TEXT,
1060 SupportType.INFO_TEXT }) {
1061 BasicSupport support = getSupport(type);
1062 if (support != null && support.supports(url)) {
1063 return support;
1064 }
1065 }
1066
1067 return null;
1068 }
1069
1070 /**
1071 * Return a {@link BasicSupport} implementation supporting the given type.
1072 *
1073 * @param type
1074 * the type
1075 *
1076 * @return an implementation that supports it, or NULL
1077 */
1078 public static BasicSupport getSupport(SupportType type) {
1079 switch (type) {
1080 case EPUB:
1081 return new Epub().setType(type);
1082 case INFO_TEXT:
1083 return new InfoText().setType(type);
1084 case FIMFICTION:
1085 return new Fimfiction().setType(type);
1086 case FANFICTION:
1087 return new Fanfiction().setType(type);
1088 case TEXT:
1089 return new Text().setType(type);
1090 case MANGAFOX:
1091 return new MangaFox().setType(type);
1092 case E621:
1093 return new E621().setType(type);
1094 case CBZ:
1095 return new Cbz().setType(type);
1096 }
1097
1098 return null;
1099 }
1100
1101 /**
1102 * Return the first line from the given input which correspond to the given
1103 * selectors.
1104 *
1105 * @param in
1106 * the input
1107 * @param needle
1108 * a string that must be found inside the target line (also
1109 * supports "^" at start to say "only if it starts with" the
1110 * needle)
1111 * @param relativeLine
1112 * the line to return based upon the target line position (-1 =
1113 * the line before, 0 = the target line...)
1114 *
1115 * @return the line
1116 */
1117 static String getLine(InputStream in, String needle, int relativeLine) {
1118 return getLine(in, needle, relativeLine, true);
1119 }
1120
1121 /**
1122 * Return a line from the given input which correspond to the given
1123 * selectors.
1124 *
1125 * @param in
1126 * the input
1127 * @param needle
1128 * a string that must be found inside the target line (also
1129 * supports "^" at start to say "only if it starts with" the
1130 * needle)
1131 * @param relativeLine
1132 * the line to return based upon the target line position (-1 =
1133 * the line before, 0 = the target line...)
1134 * @param first
1135 * takes the first result (as opposed to the last one, which will
1136 * also always spend the input)
1137 *
1138 * @return the line
1139 */
1140 static String getLine(InputStream in, String needle, int relativeLine,
1141 boolean first) {
1142 String rep = null;
1143
1144 try {
1145 in.reset();
1146 } catch (IOException e) {
1147 Instance.syserr(e);
1148 }
1149
1150 List<String> lines = new ArrayList<String>();
1151 @SuppressWarnings("resource")
1152 Scanner scan = new Scanner(in, "UTF-8");
1153 int index = -1;
1154 scan.useDelimiter("\\n");
1155 while (scan.hasNext()) {
1156 lines.add(scan.next());
1157
1158 if (index == -1) {
1159 if (needle.startsWith("^")) {
1160 if (lines.get(lines.size() - 1).startsWith(
1161 needle.substring(1))) {
1162 index = lines.size() - 1;
1163 }
1164
1165 } else {
1166 if (lines.get(lines.size() - 1).contains(needle)) {
1167 index = lines.size() - 1;
1168 }
1169 }
1170 }
1171
1172 if (index >= 0 && index + relativeLine < lines.size()) {
1173 rep = lines.get(index + relativeLine);
1174 if (first) {
1175 break;
1176 }
1177 }
1178 }
1179
1180 return rep;
1181 }
1182 }