TUI: better display of stories
[nikiroo-utils.git] / src / be / nikiroo / fanfix / output / BasicOutput.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix.output;
2
3import java.io.File;
4import java.io.IOException;
bee7dffe
NR
5import java.util.ArrayList;
6import java.util.List;
08fe2e33
NR
7
8import be.nikiroo.fanfix.Instance;
9import be.nikiroo.fanfix.bundles.StringId;
10import be.nikiroo.fanfix.data.Chapter;
11import be.nikiroo.fanfix.data.Paragraph;
08fe2e33 12import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
bee7dffe 13import be.nikiroo.fanfix.data.Story;
3b2b638f 14import be.nikiroo.utils.Progress;
08fe2e33
NR
15
16/**
17 * This class is the base class used by the other output classes. It can be used
18 * outside of this package, and have static method that you can use to get
19 * access to the correct support class.
20 *
21 * @author niki
22 */
23public abstract class BasicOutput {
24 /**
25 * The supported output types for which we can get a {@link BasicOutput}
26 * object.
27 *
28 * @author niki
29 */
30 public enum OutputType {
31 /** EPUB files created with this program */
32 EPUB,
33 /** Pure text file with some rules */
34 TEXT,
35 /** TEXT but with associated .info file */
36 INFO_TEXT,
37 /** DEBUG output to console */
38 SYSOUT,
39 /** ZIP with (PNG) images */
40 CBZ,
41 /** LaTeX file with "book" template */
2206ef66
NR
42 LATEX,
43 /** HTML files in a dedicated directory */
44 HTML,
45
46 ;
47
211f7ddb 48 @Override
08fe2e33
NR
49 public String toString() {
50 return super.toString().toLowerCase();
51 }
52
53 /**
54 * A description of this output type.
55 *
4d205683
NR
56 * @param longDesc
57 * TRUE for the long description, FALSE for the short one
58 *
08fe2e33
NR
59 * @return the description
60 */
4d205683
NR
61 public String getDesc(boolean longDesc) {
62 StringId id = longDesc ? StringId.OUTPUT_DESC
63 : StringId.OUTPUT_DESC_SHORT;
64
65 String desc = Instance.getTrans().getStringX(id, this.name());
08fe2e33
NR
66
67 if (desc == null) {
4d205683 68 desc = Instance.getTrans().getString(id, this);
08fe2e33
NR
69 }
70
b2612f9d
NR
71 if (desc == null) {
72 desc = this.toString();
73 }
74
08fe2e33
NR
75 return desc;
76 }
77
2206ef66
NR
78 /**
79 * The default extension to add to the output files.
80 *
10d558d2 81 * @param readerTarget
f7460e4c
NR
82 * TRUE to point to the main {@link Story} entry point for a
83 * reader (for instance, the main entry point if this
84 * {@link Story} is in a directory bundle), FALSE to point to
85 * the main file even if it is a directory for instance
10d558d2 86 *
2206ef66
NR
87 * @return the extension
88 */
10d558d2 89 public String getDefaultExtension(boolean readerTarget) {
925298fd 90 BasicOutput output = BasicOutput.getOutput(this, false, false);
2206ef66 91 if (output != null) {
10d558d2 92 return output.getDefaultExtension(readerTarget);
2206ef66
NR
93 }
94
95 return null;
96 }
97
08fe2e33 98 /**
0efd25e3
NR
99 * Call {@link OutputType#valueOf(String)} after conversion to upper
100 * case.
08fe2e33
NR
101 *
102 * @param typeName
103 * the possible type name
104 *
105 * @return NULL or the type
106 */
107 public static OutputType valueOfUC(String typeName) {
108 return OutputType.valueOf(typeName == null ? null : typeName
109 .toUpperCase());
110 }
111
112 /**
0efd25e3 113 * Call {@link OutputType#valueOf(String)} after conversion to upper
e604986c 114 * case but return def for NULL and empty instead of raising an
0efd25e3 115 * exception.
08fe2e33
NR
116 *
117 * @param typeName
118 * the possible type name
e604986c
NR
119 * @param def
120 * the default value
08fe2e33
NR
121 *
122 * @return NULL or the type
123 */
e604986c 124 public static OutputType valueOfNullOkUC(String typeName, OutputType def) {
a6395bef 125 if (typeName == null || typeName.isEmpty()) {
e604986c 126 return def;
08fe2e33
NR
127 }
128
129 return OutputType.valueOfUC(typeName);
130 }
131
132 /**
0efd25e3 133 * Call {@link OutputType#valueOf(String)} after conversion to upper
e604986c 134 * case but return def in case of error instead of raising an exception.
08fe2e33
NR
135 *
136 * @param typeName
137 * the possible type name
e604986c
NR
138 * @param def
139 * the default value
08fe2e33
NR
140 *
141 * @return NULL or the type
142 */
e604986c 143 public static OutputType valueOfAllOkUC(String typeName, OutputType def) {
08fe2e33
NR
144 try {
145 return OutputType.valueOfUC(typeName);
146 } catch (Exception e) {
e604986c 147 return def;
08fe2e33
NR
148 }
149 }
150 }
151
152 /** The creator name (this program, by me!) */
153 static final String EPUB_CREATOR = "Fanfix (by Niki)";
154
155 /** The current best name for an image */
156 private String imageName;
157 private File targetDir;
158 private String targetName;
159 private OutputType type;
160 private boolean writeCover;
161 private boolean writeInfo;
bee7dffe
NR
162 private Progress storyPg;
163 private Progress chapPg;
08fe2e33
NR
164
165 /**
166 * Process the {@link Story} into the given target.
167 *
168 * @param story
169 * the {@link Story} to export
170 * @param target
171 * the target where to save to (will not necessary be taken as is
172 * by the processor, for instance an extension can be added)
bee7dffe
NR
173 * @param pg
174 * the optional progress reporter
08fe2e33
NR
175 *
176 * @return the actual main target saved, which can be slightly different
177 * that the input one
178 *
179 * @throws IOException
180 * in case of I/O error
181 */
bee7dffe
NR
182 public File process(Story story, String target, Progress pg)
183 throws IOException {
184 storyPg = pg;
185
d5a7153c
NR
186 File targetDir = null;
187 String targetName = null;
188 if (target != null) {
189 target = new File(target).getAbsolutePath();
190 targetDir = new File(target).getParentFile();
191 targetName = new File(target).getName();
192
193 String ext = getDefaultExtension(false);
194 if (ext != null && !ext.isEmpty()) {
195 if (targetName.toLowerCase().endsWith(ext)) {
196 targetName = targetName.substring(0, targetName.length()
197 - ext.length());
198 }
08fe2e33
NR
199 }
200 }
201
202 return process(story, targetDir, targetName);
203 }
204
205 /**
206 * Process the {@link Story} into the given target.
207 * <p>
208 * This method is expected to be overridden in most cases.
209 *
210 * @param story
211 * the {@link Story} to export
212 * @param targetDir
213 * the target dir where to save to
214 * @param targetName
215 * the target filename (will not necessary be taken as is by the
216 * processor, for instance an extension can be added)
217 *
bee7dffe 218 *
08fe2e33
NR
219 * @return the actual main target saved, which can be slightly different
220 * that the input one
221 *
222 * @throws IOException
223 * in case of I/O error
224 */
225 protected File process(Story story, File targetDir, String targetName)
226 throws IOException {
227 this.targetDir = targetDir;
228 this.targetName = targetName;
229
230 writeStory(story);
231
232 return null;
233 }
234
235 /**
236 * The output type.
237 *
238 * @return the type
239 */
240 public OutputType getType() {
241 return type;
242 }
243
5083ef84
NR
244 /**
245 * Enable the creation of a .info file next to the resulting processed file.
246 *
247 * @return TRUE to enable it
248 */
249 protected boolean isWriteInfo() {
250 return writeInfo;
251 }
252
253 /**
254 * Enable the creation of a cover file next to the resulting processed file
255 * if possible.
256 *
257 * @return TRUE to enable it
258 */
259 protected boolean isWriteCover() {
260 return writeCover;
261 }
262
08fe2e33
NR
263 /**
264 * The output type.
265 *
266 * @param type
267 * the new type
0efd25e3
NR
268 * @param writeCover
269 * TRUE to enable the creation of a cover if possible
925298fd
NR
270 * @param writeInfo
271 * TRUE to enable the creation of a .info file
08fe2e33
NR
272 *
273 * @return this
274 */
925298fd
NR
275 protected BasicOutput setType(OutputType type, boolean writeInfo,
276 boolean writeCover) {
08fe2e33 277 this.type = type;
08fe2e33 278 this.writeInfo = writeInfo;
925298fd 279 this.writeCover = writeCover;
08fe2e33
NR
280
281 return this;
282 }
283
284 /**
285 * The default extension to add to the output files.
08fe2e33 286 *
10d558d2 287 * @param readerTarget
f7460e4c
NR
288 * TRUE to point to the main {@link Story} entry point for a
289 * reader (for instance, the main entry point if this
290 * {@link Story} is in a directory bundle), FALSE to point to the
291 * main file even if it is a directory for instance
10d558d2 292 *
08fe2e33
NR
293 * @return the extension
294 */
211f7ddb
NR
295 public String getDefaultExtension(
296 @SuppressWarnings("unused") boolean readerTarget) {
08fe2e33
NR
297 return "";
298 }
299
211f7ddb 300 @SuppressWarnings("unused")
08fe2e33
NR
301 protected void writeStoryHeader(Story story) throws IOException {
302 }
303
211f7ddb 304 @SuppressWarnings("unused")
08fe2e33
NR
305 protected void writeChapterHeader(Chapter chap) throws IOException {
306 }
307
211f7ddb 308 @SuppressWarnings("unused")
08fe2e33
NR
309 protected void writeParagraphHeader(Paragraph para) throws IOException {
310 }
311
211f7ddb 312 @SuppressWarnings("unused")
08fe2e33
NR
313 protected void writeStoryFooter(Story story) throws IOException {
314 }
315
211f7ddb 316 @SuppressWarnings("unused")
08fe2e33
NR
317 protected void writeChapterFooter(Chapter chap) throws IOException {
318 }
319
211f7ddb 320 @SuppressWarnings("unused")
08fe2e33
NR
321 protected void writeParagraphFooter(Paragraph para) throws IOException {
322 }
323
324 protected void writeStory(Story story) throws IOException {
bee7dffe
NR
325 if (storyPg == null) {
326 storyPg = new Progress(0, story.getChapters().size() + 2);
327 } else {
328 storyPg.setMinMax(0, story.getChapters().size() + 2);
329 }
330
08fe2e33
NR
331 String chapterNameNum = String.format("%03d", 0);
332 String paragraphNumber = String.format("%04d", 0);
ecfb936e 333 imageName = paragraphNumber + "_" + chapterNameNum;
08fe2e33 334
fe999aa4 335 if (story.getMeta() != null) {
68686a37 336 story.getMeta().setType("" + getType());
fe999aa4 337 }
68686a37 338
276f95c6 339 if (isWriteCover()) {
08fe2e33
NR
340 InfoCover.writeCover(targetDir, targetName, story.getMeta());
341 }
276f95c6 342 if (isWriteInfo()) {
08fe2e33
NR
343 InfoCover.writeInfo(targetDir, targetName, story.getMeta());
344 }
345
bee7dffe
NR
346 storyPg.setProgress(1);
347
348 List<Progress> chapPgs = new ArrayList<Progress>(story.getChapters()
349 .size());
08fe2e33 350 for (Chapter chap : story) {
bee7dffe
NR
351 chapPg = new Progress(0, chap.getParagraphs().size());
352 storyPg.addProgress(chapPg, 1);
353 chapPgs.add(chapPg);
354 chapPg = null;
355 }
356
357 writeStoryHeader(story);
358 for (int i = 0; i < story.getChapters().size(); i++) {
359 chapPg = chapPgs.get(i);
360 writeChapter(story.getChapters().get(i));
361 chapPg.setProgress(chapPg.getMax());
362 chapPg = null;
08fe2e33
NR
363 }
364 writeStoryFooter(story);
bee7dffe
NR
365
366 storyPg.setProgress(storyPg.getMax());
367 storyPg = null;
08fe2e33
NR
368 }
369
370 protected void writeChapter(Chapter chap) throws IOException {
371 String chapterNameNum;
372 if (chap.getName() == null || chap.getName().isEmpty()) {
373 chapterNameNum = String.format("%03d", chap.getNumber());
374 } else {
375 chapterNameNum = String.format("%03d", chap.getNumber()) + "_"
376 + chap.getName().replace(" ", "_");
377 }
378
379 int num = 0;
380 String paragraphNumber = String.format("%04d", num++);
16a81ef7 381 imageName = chapterNameNum + "_" + paragraphNumber;
08fe2e33
NR
382
383 writeChapterHeader(chap);
bee7dffe 384 int i = 1;
08fe2e33
NR
385 for (Paragraph para : chap) {
386 paragraphNumber = String.format("%04d", num++);
16a81ef7 387 imageName = chapterNameNum + "_" + paragraphNumber;
08fe2e33 388 writeParagraph(para);
bee7dffe
NR
389 if (chapPg != null) {
390 chapPg.setProgress(i++);
391 }
08fe2e33
NR
392 }
393 writeChapterFooter(chap);
394 }
395
396 protected void writeParagraph(Paragraph para) throws IOException {
397 writeParagraphHeader(para);
398 writeTextLine(para.getType(), para.getContent());
399 writeParagraphFooter(para);
400 }
401
211f7ddb 402 @SuppressWarnings("unused")
08fe2e33
NR
403 protected void writeTextLine(ParagraphType type, String line)
404 throws IOException {
405 }
406
407 /**
408 * Return the current best guess for an image name, based upon the current
409 * {@link Chapter} and {@link Paragraph}.
410 *
411 * @param prefix
412 * add the original target name as a prefix
413 *
414 * @return the guessed name
415 */
416 protected String getCurrentImageBestName(boolean prefix) {
417 if (prefix) {
418 return targetName + "_" + imageName;
419 }
420
421 return imageName;
422 }
423
424 /**
425 * Return the given word or sentence as <b>bold</b>.
426 *
427 * @param word
428 * the input
429 *
430 * @return the bold output
431 */
432 protected String enbold(String word) {
433 return word;
434 }
435
436 /**
437 * Return the given word or sentence as <i>italic</i>.
438 *
439 * @param word
440 * the input
441 *
442 * @return the italic output
443 */
444 protected String italize(String word) {
445 return word;
446 }
447
448 /**
449 * Decorate the given text with <b>bold</b> and <i>italic</i> words,
450 * according to {@link BasicOutput#enbold(String)} and
451 * {@link BasicOutput#italize(String)}.
452 *
453 * @param text
454 * the input
455 *
456 * @return the decorated output
457 */
458 protected String decorateText(String text) {
459 StringBuilder builder = new StringBuilder();
460
461 int bold = -1;
462 int italic = -1;
463 char prev = '\0';
464 for (char car : text.toCharArray()) {
465 switch (car) {
466 case '*':
467 if (bold >= 0 && prev != ' ') {
468 String data = builder.substring(bold);
469 builder.setLength(bold);
470 builder.append(enbold(data));
471 bold = -1;
472 } else if (bold < 0
473 && (prev == ' ' || prev == '\0' || prev == '\n')) {
474 bold = builder.length();
475 } else {
476 builder.append(car);
477 }
478
479 break;
480 case '_':
481 if (italic >= 0 && prev != ' ') {
482 String data = builder.substring(italic);
483 builder.setLength(italic);
484 builder.append(enbold(data));
485 italic = -1;
486 } else if (italic < 0
487 && (prev == ' ' || prev == '\0' || prev == '\n')) {
488 italic = builder.length();
489 } else {
490 builder.append(car);
491 }
492
493 break;
494 default:
495 builder.append(car);
496 break;
497 }
498
499 prev = car;
500 }
501
502 if (bold >= 0) {
503 builder.insert(bold, '*');
504 }
505
506 if (italic >= 0) {
507 builder.insert(italic, '_');
508 }
509
510 return builder.toString();
511 }
512
513 /**
514 * Return a {@link BasicOutput} object compatible with the given
515 * {@link OutputType}.
516 *
517 * @param type
518 * the type
925298fd
NR
519 * @param writeCover
520 * TRUE to enable the creation of a cover if possible to be saved
521 * next to the main target file
522 * @param writeInfo
523 * TRUE to enable the creation of a .info file to be saved next
08fe2e33
NR
524 * to the main target file
525 *
526 * @return the {@link BasicOutput}
527 */
925298fd
NR
528 public static BasicOutput getOutput(OutputType type, boolean writeInfo,
529 boolean writeCover) {
08fe2e33
NR
530 if (type != null) {
531 switch (type) {
532 case EPUB:
925298fd 533 return new Epub().setType(type, writeInfo, writeCover);
08fe2e33 534 case TEXT:
925298fd 535 return new Text().setType(type, writeInfo, true);
08fe2e33
NR
536 case INFO_TEXT:
537 return new InfoText().setType(type, true, true);
538 case SYSOUT:
539 return new Sysout().setType(type, false, false);
540 case CBZ:
925298fd 541 return new Cbz().setType(type, writeInfo, writeCover);
08fe2e33 542 case LATEX:
925298fd 543 return new LaTeX().setType(type, writeInfo, writeCover);
2206ef66 544 case HTML:
925298fd 545 return new Html().setType(type, writeInfo, writeCover);
08fe2e33
NR
546 }
547 }
548
549 return null;
550 }
551}