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