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