Version 1.1.0
[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.StringUtils;
27 import be.nikiroo.utils.ui.Progress;
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 i++;
382 }
383 } else {
384 pg.setProgress(100);
385 }
386
387 return story;
388
389 } finally {
390 try {
391 close();
392 } catch (IOException e) {
393 Instance.syserr(e);
394 }
395
396 if (in != null) {
397 in.close();
398 }
399
400 currentReferer = null;
401 }
402 }
403
404 /**
405 * The support type.$
406 *
407 * @return the type
408 */
409 public SupportType getType() {
410 return type;
411 }
412
413 /**
414 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
415 * the current {@link URL} we work on.
416 *
417 * @return the referer
418 */
419 public URL getCurrentReferer() {
420 return currentReferer;
421 }
422
423 /**
424 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
425 * the current {@link URL} we work on.
426 *
427 * @param currentReferer
428 * the new referer
429 */
430 protected void setCurrentReferer(URL currentReferer) {
431 this.currentReferer = currentReferer;
432 }
433
434 /**
435 * The support type.
436 *
437 * @param type
438 * the new type
439 *
440 * @return this
441 */
442 protected BasicSupport setType(SupportType type) {
443 this.type = type;
444 return this;
445 }
446
447 /**
448 * Prepare the support if needed before processing.
449 *
450 * @param source
451 * the source of the story
452 * @param in
453 * the input (the main resource)
454 *
455 * @throws IOException
456 * on I/O error
457 */
458 protected void preprocess(URL source, InputStream in) throws IOException {
459 }
460
461 /**
462 * Now that we have processed the {@link Story}, close the resources if any.
463 *
464 * @throws IOException
465 * on I/O error
466 */
467 protected void close() throws IOException {
468 }
469
470 /**
471 * Create a {@link Chapter} object from the given information, formatting
472 * the content as it should be.
473 *
474 * @param number
475 * the chapter number
476 * @param name
477 * the chapter name
478 * @param content
479 * the chapter content
480 *
481 * @return the {@link Chapter}
482 *
483 * @throws IOException
484 * in case of I/O error
485 */
486 protected Chapter makeChapter(URL source, int number, String name,
487 String content) throws IOException {
488 // Chapter name: process it correctly, then remove the possible
489 // redundant "Chapter x: " in front of it
490 String chapterName = processPara(name).getContent().trim();
491 for (String lang : Instance.getConfig().getString(Config.CHAPTER)
492 .split(",")) {
493 String chapterWord = Instance.getConfig().getStringX(
494 Config.CHAPTER, lang);
495 if (chapterName.startsWith(chapterWord)) {
496 chapterName = chapterName.substring(chapterWord.length())
497 .trim();
498 break;
499 }
500 }
501
502 if (chapterName.startsWith(Integer.toString(number))) {
503 chapterName = chapterName.substring(
504 Integer.toString(number).length()).trim();
505 }
506
507 if (chapterName.startsWith(":")) {
508 chapterName = chapterName.substring(1).trim();
509 }
510 //
511
512 Chapter chap = new Chapter(number, chapterName);
513
514 if (content == null) {
515 return chap;
516 }
517
518 if (isHtml()) {
519 // Special <HR> processing:
520 content = content.replaceAll("(<hr [^>]*>)|(<hr/>)|(<hr>)",
521 "\n* * *\n");
522 }
523
524 InputStream in = new ByteArrayInputStream(content.getBytes("UTF-8"));
525 try {
526 @SuppressWarnings("resource")
527 Scanner scan = new Scanner(in, "UTF-8");
528 scan.useDelimiter("(\\n|</p>)"); // \n for test, </p> for html
529
530 List<Paragraph> paras = new ArrayList<Paragraph>();
531 while (scan.hasNext()) {
532 String line = scan.next().trim();
533 boolean image = false;
534 if (line.startsWith("[") && line.endsWith("]")) {
535 URL url = getImageUrl(this, source,
536 line.substring(1, line.length() - 1).trim());
537 if (url != null) {
538 paras.add(new Paragraph(url));
539 image = true;
540 }
541 }
542
543 if (!image) {
544 paras.add(processPara(line));
545 }
546 }
547
548 // Check quotes for "bad" format
549 List<Paragraph> newParas = new ArrayList<Paragraph>();
550 for (Paragraph para : paras) {
551 newParas.addAll(requotify(para));
552 }
553 paras = newParas;
554
555 // Remove double blanks/brks
556 boolean space = false;
557 boolean brk = true;
558 for (int i = 0; i < paras.size(); i++) {
559 Paragraph para = paras.get(i);
560 boolean thisSpace = para.getType() == ParagraphType.BLANK;
561 boolean thisBrk = para.getType() == ParagraphType.BREAK;
562
563 if (space && thisBrk) {
564 paras.remove(i - 1);
565 i--;
566 } else if ((space || brk) && (thisSpace || thisBrk)) {
567 paras.remove(i);
568 i--;
569 }
570
571 space = thisSpace;
572 brk = thisBrk;
573 }
574
575 // Remove blank/brk at start
576 if (paras.size() > 0
577 && (paras.get(0).getType() == ParagraphType.BLANK || paras
578 .get(0).getType() == ParagraphType.BREAK)) {
579 paras.remove(0);
580 }
581
582 // Remove blank/brk at end
583 int last = paras.size() - 1;
584 if (paras.size() > 0
585 && (paras.get(last).getType() == ParagraphType.BLANK || paras
586 .get(last).getType() == ParagraphType.BREAK)) {
587 paras.remove(last);
588 }
589
590 chap.setParagraphs(paras);
591
592 return chap;
593 } finally {
594 in.close();
595 }
596 }
597
598 static BufferedImage getDefaultCover(String subject) {
599 if (subject != null && !subject.isEmpty()
600 && Instance.getCoverDir() != null) {
601 try {
602 File fileCover = new File(Instance.getCoverDir(), subject);
603 return getImage(null, fileCover.toURI().toURL(), subject);
604 } catch (MalformedURLException e) {
605 }
606 }
607
608 return null;
609 }
610
611 /**
612 * Return the list of supported image extensions.
613 *
614 * @return the extensions
615 */
616 static String[] getImageExt(boolean emptyAllowed) {
617 if (emptyAllowed) {
618 return new String[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
619 } else {
620 return new String[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
621 }
622 }
623
624 static BufferedImage getImage(BasicSupport support, URL source, String line) {
625 URL url = getImageUrl(support, source, line);
626 if (url != null) {
627 InputStream in = null;
628 try {
629 in = Instance.getCache().open(url, getSupport(url), true);
630 return IOUtils.toImage(in);
631 } catch (IOException e) {
632 } finally {
633 if (in != null) {
634 try {
635 in.close();
636 } catch (IOException e) {
637 }
638 }
639 }
640 }
641
642 return null;
643 }
644
645 /**
646 * Check if the given resource can be a local image or a remote image, then
647 * refresh the cache with it if it is.
648 *
649 * @param source
650 * the story source
651 * @param line
652 * the resource to check
653 *
654 * @return the image URL if found, or NULL
655 *
656 */
657 static URL getImageUrl(BasicSupport support, URL source, String line) {
658 URL url = null;
659
660 if (line != null) {
661 // try for files
662 String path = null;
663 if (source != null) {
664 path = new File(source.getFile()).getParent();
665 try {
666 String basePath = new File(new File(path), line.trim())
667 .getAbsolutePath();
668 for (String ext : getImageExt(true)) {
669 if (new File(basePath + ext).exists()) {
670 url = new File(basePath + ext).toURI().toURL();
671 }
672 }
673 } catch (Exception e) {
674 // Nothing to do here
675 }
676 }
677
678 if (url == null) {
679 // try for URLs
680 try {
681 for (String ext : getImageExt(true)) {
682 if (Instance.getCache().check(new URL(line + ext))) {
683 url = new URL(line + ext);
684 break;
685 }
686 }
687
688 // try out of cache
689 if (url == null) {
690 for (String ext : getImageExt(true)) {
691 try {
692 url = new URL(line + ext);
693 Instance.getCache().refresh(url, support, true);
694 break;
695 } catch (IOException e) {
696 // no image with this ext
697 url = null;
698 }
699 }
700 }
701 } catch (MalformedURLException e) {
702 // Not an url
703 }
704 }
705
706 // refresh the cached file
707 if (url != null) {
708 try {
709 Instance.getCache().refresh(url, support, true);
710 } catch (IOException e) {
711 // woops, broken image
712 url = null;
713 }
714 }
715 }
716
717 return url;
718 }
719
720 protected InputStream reset(InputStream in) {
721 try {
722 in.reset();
723 } catch (IOException e) {
724 }
725 return in;
726 }
727
728 /**
729 * Reset then return {@link BasicSupport#in}.
730 *
731 * @return {@link BasicSupport#in}
732 */
733 protected InputStream getInput() {
734 return reset(in);
735 }
736
737 /**
738 * Fix the author name if it is prefixed with some "by" {@link String}.
739 *
740 * @param author
741 * the author with a possible prefix
742 *
743 * @return the author without prefixes
744 */
745 protected String fixAuthor(String author) {
746 if (author != null) {
747 for (String suffix : new String[] { " ", ":" }) {
748 for (String byString : Instance.getConfig()
749 .getString(Config.BYS).split(",")) {
750 byString += suffix;
751 if (author.toUpperCase().startsWith(byString.toUpperCase())) {
752 author = author.substring(byString.length()).trim();
753 }
754 }
755 }
756
757 // Special case (without suffix):
758 if (author.startsWith("©")) {
759 author = author.substring(1);
760 }
761 }
762
763 return author;
764 }
765
766 /**
767 * Check quotes for bad format (i.e., quotes with normal paragraphs inside)
768 * and requotify them (i.e., separate them into QUOTE paragraphs and other
769 * paragraphs (quotes or not)).
770 *
771 * @param para
772 * the paragraph to requotify (not necessaraly a quote)
773 *
774 * @return the correctly (or so we hope) quotified paragraphs
775 */
776 private List<Paragraph> requotify(Paragraph para) {
777 List<Paragraph> newParas = new ArrayList<Paragraph>();
778
779 if (para.getType() == ParagraphType.QUOTE
780 && para.getContent().length() > 2) {
781 String line = para.getContent();
782 boolean singleQ = line.startsWith("" + openQuote);
783 boolean doubleQ = line.startsWith("" + openDoubleQuote);
784
785 // Do not try when more than one quote at a time
786 // (some stories are not easily readable if we do)
787 if (singleQ
788 && line.indexOf(closeQuote, 1) < line
789 .lastIndexOf(closeQuote)) {
790 newParas.add(para);
791 return newParas;
792 }
793 if (doubleQ
794 && line.indexOf(closeDoubleQuote, 1) < line
795 .lastIndexOf(closeDoubleQuote)) {
796 newParas.add(para);
797 return newParas;
798 }
799 //
800
801 if (!singleQ && !doubleQ) {
802 line = openDoubleQuote + line + closeDoubleQuote;
803 newParas.add(new Paragraph(ParagraphType.QUOTE, line));
804 } else {
805 char open = singleQ ? openQuote : openDoubleQuote;
806 char close = singleQ ? closeQuote : closeDoubleQuote;
807
808 int posDot = -1;
809 boolean inQuote = false;
810 int i = 0;
811 for (char car : line.toCharArray()) {
812 if (car == open) {
813 inQuote = true;
814 } else if (car == close) {
815 inQuote = false;
816 } else if (car == '.' && !inQuote) {
817 posDot = i;
818 break;
819 }
820 i++;
821 }
822
823 if (posDot >= 0) {
824 String rest = line.substring(posDot + 1).trim();
825 line = line.substring(0, posDot + 1).trim();
826 newParas.add(new Paragraph(ParagraphType.QUOTE, line));
827 if (!rest.isEmpty()) {
828 newParas.addAll(requotify(processPara(rest)));
829 }
830 } else {
831 newParas.add(para);
832 }
833 }
834 } else {
835 newParas.add(para);
836 }
837
838 return newParas;
839 }
840
841 /**
842 * Process a {@link Paragraph} from a raw line of text.
843 * <p>
844 * Will also fix quotes and HTML encoding if needed.
845 *
846 * @param line
847 * the raw line
848 *
849 * @return the processed {@link Paragraph}
850 */
851 private Paragraph processPara(String line) {
852 line = ifUnhtml(line).trim();
853
854 boolean space = true;
855 boolean brk = true;
856 boolean quote = false;
857 boolean tentativeCloseQuote = false;
858 char prev = '\0';
859 int dashCount = 0;
860
861 StringBuilder builder = new StringBuilder();
862 for (char car : line.toCharArray()) {
863 if (car != '-') {
864 if (dashCount > 0) {
865 // dash, ndash and mdash: - – —
866 // currently: always use mdash
867 builder.append(dashCount == 1 ? '-' : '—');
868 }
869 dashCount = 0;
870 }
871
872 if (tentativeCloseQuote) {
873 tentativeCloseQuote = false;
874 if ((car >= 'a' && car <= 'z') || (car >= 'A' && car <= 'Z')
875 || (car >= '0' && car <= '9')) {
876 builder.append("'");
877 } else {
878 builder.append(closeQuote);
879 }
880 }
881
882 switch (car) {
883 case ' ': // note: unbreakable space
884 case ' ':
885 case '\t':
886 case '\n': // just in case
887 case '\r': // just in case
888 builder.append(' ');
889 break;
890
891 case '\'':
892 if (space || (brk && quote)) {
893 quote = true;
894 builder.append(openQuote);
895 } else if (prev == ' ') {
896 builder.append(openQuote);
897 } else {
898 // it is a quote ("I'm off") or a 'quote' ("This
899 // 'good' restaurant"...)
900 tentativeCloseQuote = true;
901 }
902 break;
903
904 case '"':
905 if (space || (brk && quote)) {
906 quote = true;
907 builder.append(openDoubleQuote);
908 } else if (prev == ' ') {
909 builder.append(openDoubleQuote);
910 } else {
911 builder.append(closeDoubleQuote);
912 }
913 break;
914
915 case '-':
916 if (space) {
917 quote = true;
918 } else {
919 dashCount++;
920 }
921 space = false;
922 break;
923
924 case '*':
925 case '~':
926 case '/':
927 case '\\':
928 case '<':
929 case '>':
930 case '=':
931 case '+':
932 case '_':
933 case '–':
934 case '—':
935 space = false;
936 builder.append(car);
937 break;
938
939 case '‘':
940 case '`':
941 case '‹':
942 case '﹁':
943 case '〈':
944 case '「':
945 if (space || (brk && quote)) {
946 quote = true;
947 builder.append(openQuote);
948 } else {
949 builder.append(openQuote);
950 }
951 space = false;
952 brk = false;
953 break;
954
955 case '’':
956 case '›':
957 case '﹂':
958 case '〉':
959 case '」':
960 space = false;
961 brk = false;
962 builder.append(closeQuote);
963 break;
964
965 case '«':
966 case '“':
967 case '﹃':
968 case '《':
969 case '『':
970 if (space || (brk && quote)) {
971 quote = true;
972 builder.append(openDoubleQuote);
973 } else {
974 builder.append(openDoubleQuote);
975 }
976 space = false;
977 brk = false;
978 break;
979
980 case '»':
981 case '”':
982 case '﹄':
983 case '》':
984 case '』':
985 space = false;
986 brk = false;
987 builder.append(closeDoubleQuote);
988 break;
989
990 default:
991 space = false;
992 brk = false;
993 builder.append(car);
994 break;
995 }
996
997 prev = car;
998 }
999
1000 if (tentativeCloseQuote) {
1001 tentativeCloseQuote = false;
1002 builder.append(closeQuote);
1003 }
1004
1005 line = builder.toString().trim();
1006
1007 ParagraphType type = ParagraphType.NORMAL;
1008 if (space) {
1009 type = ParagraphType.BLANK;
1010 } else if (brk) {
1011 type = ParagraphType.BREAK;
1012 } else if (quote) {
1013 type = ParagraphType.QUOTE;
1014 }
1015
1016 return new Paragraph(type, line);
1017 }
1018
1019 /**
1020 * Remove the HTML from the inpit <b>if</b> {@link BasicSupport#isHtml()} is
1021 * true.
1022 *
1023 * @param input
1024 * the input
1025 *
1026 * @return the no html version if needed
1027 */
1028 private String ifUnhtml(String input) {
1029 if (isHtml() && input != null) {
1030 return StringUtils.unhtml(input);
1031 }
1032
1033 return input;
1034 }
1035
1036 /**
1037 * Return a {@link BasicSupport} implementation supporting the given
1038 * resource if possible.
1039 *
1040 * @param url
1041 * the story resource
1042 *
1043 * @return an implementation that supports it, or NULL
1044 */
1045 public static BasicSupport getSupport(URL url) {
1046 if (url == null) {
1047 return null;
1048 }
1049
1050 // TEXT and INFO_TEXT always support files (not URLs though)
1051 for (SupportType type : SupportType.values()) {
1052 if (type != SupportType.TEXT && type != SupportType.INFO_TEXT) {
1053 BasicSupport support = getSupport(type);
1054 if (support != null && support.supports(url)) {
1055 return support;
1056 }
1057 }
1058 }
1059
1060 for (SupportType type : new SupportType[] { SupportType.TEXT,
1061 SupportType.INFO_TEXT }) {
1062 BasicSupport support = getSupport(type);
1063 if (support != null && support.supports(url)) {
1064 return support;
1065 }
1066 }
1067
1068 return null;
1069 }
1070
1071 /**
1072 * Return a {@link BasicSupport} implementation supporting the given type.
1073 *
1074 * @param type
1075 * the type
1076 *
1077 * @return an implementation that supports it, or NULL
1078 */
1079 public static BasicSupport getSupport(SupportType type) {
1080 switch (type) {
1081 case EPUB:
1082 return new Epub().setType(type);
1083 case INFO_TEXT:
1084 return new InfoText().setType(type);
1085 case FIMFICTION:
1086 return new Fimfiction().setType(type);
1087 case FANFICTION:
1088 return new Fanfiction().setType(type);
1089 case TEXT:
1090 return new Text().setType(type);
1091 case MANGAFOX:
1092 return new MangaFox().setType(type);
1093 case E621:
1094 return new E621().setType(type);
1095 case CBZ:
1096 return new Cbz().setType(type);
1097 }
1098
1099 return null;
1100 }
1101
1102 /**
1103 * Return the first line from the given input which correspond to the given
1104 * selectors.
1105 *
1106 * @param in
1107 * the input
1108 * @param needle
1109 * a string that must be found inside the target line (also
1110 * supports "^" at start to say "only if it starts with" the
1111 * needle)
1112 * @param relativeLine
1113 * the line to return based upon the target line position (-1 =
1114 * the line before, 0 = the target line...)
1115 *
1116 * @return the line
1117 */
1118 static String getLine(InputStream in, String needle, int relativeLine) {
1119 return getLine(in, needle, relativeLine, true);
1120 }
1121
1122 /**
1123 * Return a line from the given input which correspond to the given
1124 * selectors.
1125 *
1126 * @param in
1127 * the input
1128 * @param needle
1129 * a string that must be found inside the target line (also
1130 * supports "^" at start to say "only if it starts with" the
1131 * needle)
1132 * @param relativeLine
1133 * the line to return based upon the target line position (-1 =
1134 * the line before, 0 = the target line...)
1135 * @param first
1136 * takes the first result (as opposed to the last one, which will
1137 * also always spend the input)
1138 *
1139 * @return the line
1140 */
1141 static String getLine(InputStream in, String needle, int relativeLine,
1142 boolean first) {
1143 String rep = null;
1144
1145 try {
1146 in.reset();
1147 } catch (IOException e) {
1148 Instance.syserr(e);
1149 }
1150
1151 List<String> lines = new ArrayList<String>();
1152 @SuppressWarnings("resource")
1153 Scanner scan = new Scanner(in, "UTF-8");
1154 int index = -1;
1155 scan.useDelimiter("\\n");
1156 while (scan.hasNext()) {
1157 lines.add(scan.next());
1158
1159 if (index == -1) {
1160 if (needle.startsWith("^")) {
1161 if (lines.get(lines.size() - 1).startsWith(
1162 needle.substring(1))) {
1163 index = lines.size() - 1;
1164 }
1165
1166 } else {
1167 if (lines.get(lines.size() - 1).contains(needle)) {
1168 index = lines.size() - 1;
1169 }
1170 }
1171 }
1172
1173 if (index >= 0 && index + relativeLine < lines.size()) {
1174 rep = lines.get(index + relativeLine);
1175 if (first) {
1176 break;
1177 }
1178 }
1179 }
1180
1181 return rep;
1182 }
1183 }