some work on proper line breaking (only where breaks are allowed)
[xreader.git] / xreaderfmt.d
blobb36a34efb85c0c2fec0a9a79520448eb109d1d2d
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module xreaderfmt;
19 import core.atomic;
20 import core.time;
22 import std.concurrency;
23 import std.stdio;
25 import iv.strex;
27 import arsd.simpledisplay;
28 import arsd.color;
29 import arsd.png;
30 import arsd.jpeg;
32 import iv.nanovg;
33 import iv.nanovg.oui.blendish;
35 import iv.vfs;
37 import xmodel;
38 import xiniz;
40 import booktext;
42 import xreadercfg;
43 import xlayouter;
46 // ////////////////////////////////////////////////////////////////////////// //
47 BookText loadBook (string fname) {
48 __gshared bool eliteShipsLoaded = false;
50 import std.path;
51 //import core.memory : GC;
52 fname = fname.expandTilde.absolutePath;
53 //writeln("loading '", fname, "'...");
54 //GC.disable();
55 auto stt = MonoTime.currTime;
56 auto book = new BookText(fname);
57 //GC.enable();
58 //GC.collect();
59 writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");
61 string[] lines;
62 string[] comments;
63 try {
64 foreach (string line; File(buildPath(RcDir, ".lastfile")).byLineCopy) {
65 if (line != fname) {
66 lines ~= line;
67 } else {
68 while (lines.length && lines[$-1].isComment) {
69 comments ~= lines[$-1];
70 lines = lines[0..$-1];
74 } catch (Exception) {}
75 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
76 auto fo = File(buildPath(RcDir, ".lastfile"), "w");
77 foreach (string s; lines) fo.writeln(s);
78 foreach_reverse (string s; comments) fo.writeln(s);
79 fo.writeln(fname);
81 stateFileName = buildPath(RcDir, fname.baseName~".rc");
83 if (!eliteShipsLoaded) {
84 loadEliteShips();
85 eliteShipsLoaded = true;
88 return book;
92 // ////////////////////////////////////////////////////////////////////////// //
93 private void formatBook (BookText book, LayText lay) {
94 assert(book !is null);
96 void dumpTree (Tag ct) {
97 while (ct !is null) {
98 writeln("tag: ", ct.name);
99 ct = ct.parent;
103 void putParaContentInternal (Tag ct) {
104 if (ct is null) return;
105 switch (ct.name) {
106 case "strong": lay.put(LayText.BoldOnCh); break;
107 case "emphasis": lay.put(LayText.ItalicOnCh); break;
108 case "image": return;
109 case "style": return;
110 case "a": break;
111 //case "p": break;
112 //case "v": break;
113 case "": lay.put(ct.text); return;
114 default: dumpTree(ct); throw new Exception("invalid content tag: '"~ct.name~"'");
116 foreach (Tag tag; ct.children) putParaContentInternal(tag);
117 switch (ct.name) {
118 case "strong": lay.put(LayText.BoldOnCh); break;
119 case "emphasis": lay.put(LayText.ItalicOnCh); break;
120 default:
124 void putTagContents (Tag ct) {
125 if (ct is null) return;
126 foreach (Tag tag; ct.children) putParaContentInternal(tag);
127 lay.put(LayText.EndParaCh);
130 void putParas (Tag ct) {
131 foreach (Tag tag; ct.children) {
132 if (tag.name.length == 0) continue;
133 if (tag.name == "p") {
134 putTagContents(tag);
135 } else if (tag.name == "empty-line") {
136 lay.put(LayText.EndLineCh);
137 } else if (tag.name == "text-author") {
138 } else {
139 dumpTree(ct);
140 throw new Exception("invalid content tag: '"~tag.name~"'");
145 void putAuthor (Tag ct) {
146 foreach (Tag tag; ct.children) {
147 if (tag.name.length == 0) continue;
148 if (tag.name == "text-author") {
149 lay.put(LayText.EndParaCh);
150 putTagContents(tag);
151 lay.put(LayText.EndParaCh);
156 void putCite (Tag ct) {
157 lay.put(LayText.CiteCh);
158 putParas(ct);
159 putAuthor(ct);
160 lay.put(LayText.NormalTextCh);
163 void putEpigraph (Tag ct) {
164 lay.put(LayText.EpigraphCh);
165 putParas(ct);
166 putAuthor(ct);
167 lay.put(LayText.NormalTextCh);
170 void putStanza (Tag ct) {
171 foreach (Tag tag; ct.children) {
172 if (tag.name.length == 0) continue;
173 if (tag.name == "text-author") continue;
174 if (tag.name == "title") { dumpTree(ct); throw new Exception("titles in stanzas are not supported yet"); }
175 if (tag.name == "subtitle") { dumpTree(ct); throw new Exception("subtitles in stanzas are not supported yet"); }
176 if (tag.name == "epigraph") { dumpTree(ct); throw new Exception("epigraphs in poems are not supported yet"); }
177 if (tag.name == "date") continue;
178 if (tag.name == "v") { putTagContents(tag); continue; }
179 dumpTree(ct);
180 throw new Exception("invalid content tag: '"~tag.name~"'");
184 void putPoem (Tag ct) {
185 lay.put(LayText.PoemCh);
186 // put epigraph (not yet)
187 // put title and subtitle
188 foreach (Tag tag; ct.children) {
189 if (tag.name == "title") {
190 lay.size = lay.size+4;
191 scope(exit) lay.size = lay.size-4;
192 putParas(tag);
193 lay.put(LayText.EndParaCh);
194 } else if (tag.name == "subtitle") {
195 lay.size = lay.size+2;
196 scope(exit) lay.size = lay.size-2;
197 putParas(tag);
198 lay.put(LayText.EndParaCh);
201 foreach (Tag tag; ct.children) {
202 if (tag.name.length == 0) continue;
203 if (tag.name == "text-author") continue;
204 if (tag.name == "title") continue;
205 if (tag.name == "subtitle") continue;
206 if (tag.name == "epigraph") { dumpTree(ct); throw new Exception("epigraphs in poems are not supported yet"); }
207 if (tag.name == "date") continue;
208 if (tag.name == "stanza") { putStanza(tag); lay.put(LayText.EndParaCh); continue; }
209 dumpTree(ct);
210 throw new Exception("invalid content tag: '"~tag.name~"'");
212 putAuthor(ct);
213 lay.put(LayText.NormalTextCh);
216 void putSection (Tag sc) {
217 bool sectionRegistered = false;
219 void registerSection (Tag tag) {
220 import std.conv : to;
221 if (sectionRegistered) return;
222 sectionRegistered = true;
223 string text = tag.textContent.xstrip;
224 lay.sections ~= lay.curWordIndex;
225 string name;
226 while (text.length) {
227 char ch = text.ptr[0];
228 text = text[1..$];
229 if (ch <= ' ' || ch == 127) {
230 if (name.length == 0) continue;
231 if (ch != '\n') ch = ' ';
232 if (ch == '\n') {
233 if (name[$-1] > ' ') name ~= "\n...";
234 text = text.xstrip;
235 } else {
236 if (name[$-1] > ' ') name ~= ' ';
238 } else {
239 name ~= ch;
242 name = xstrip(name);
243 if (name.length == 0) name = "* * *";
244 lay.sectionNames ~= name.to!dstring;
247 foreach (Tag tag; sc.children) {
248 if (tag.name.length == 0) continue;
249 if (tag.name == "title") {
250 lay.size = lay.size+4;
251 scope(exit) lay.size = lay.size-4;
252 lay.put(LayText.TitleCh);
253 registerSection(tag);
254 putParas(tag);
255 lay.put(LayText.NormalTextCh);
256 } else if (tag.name == "subtitle") {
257 lay.size = lay.size+2;
258 scope(exit) lay.size = lay.size-2;
259 lay.put(LayText.SubtitleCh);
260 registerSection(tag);
261 putParas(tag);
262 lay.put(LayText.NormalTextCh);
263 } else if (tag.name == "epigraph") {
264 putEpigraph(tag);
265 } else if (tag.name == "section") {
266 lay.put(LayText.EndParaCh);
267 putSection(tag);
268 } else if (tag.name == "p") {
269 putTagContents(tag);
270 } else if (tag.name == "empty-line") {
271 lay.put(LayText.EndParaCh);
272 } else if (tag.name == "cite") {
273 putCite(tag);
274 } else if (tag.name == "table") {
275 lay.put(LayText.CiteCh);
276 lay.put("TABLE SKIPPED");
277 lay.put(LayText.NormalTextCh);
278 } else if (tag.name == "poem") {
279 putPoem(tag);
280 } else if (tag.name == "image") {
281 } else if (tag.name == "style") {
282 } else {
283 dumpTree(tag);
284 throw new Exception("invalid tag in section: '"~tag.name~"'");
289 foreach (Tag tag; book.content.children) {
290 if (tag.name == "title") {
291 lay.size = lay.size+4;
292 scope(exit) lay.size = lay.size-4;
293 lay.put(LayText.EndParaCh);
294 lay.put(LayText.EndParaCh);
295 lay.put(LayText.TitleCh);
296 putParas(tag);
297 lay.put(LayText.NormalTextCh);
298 } else if (tag.name == "subtitle") {
299 lay.size = lay.size+2;
300 scope(exit) lay.size = lay.size-2;
301 lay.put(LayText.SubtitleCh);
302 putParas(tag);
303 lay.put(LayText.NormalTextCh);
304 } else if (tag.name == "epigraph") {
305 putEpigraph(tag);
306 } else if (tag.name == "section") {
307 putSection(tag);
311 lay.finalize();
315 // ////////////////////////////////////////////////////////////////////////// //
316 private __gshared LayoutFonts laf; // layouter font stash
319 // ////////////////////////////////////////////////////////////////////////// //
320 private void loadFmtFonts () {
321 laf = new LayoutFonts();
323 void loadFont (string name, string path) {
324 int fid = laf.fs.fonsAddFont(name, path);
325 if (fid < 0) throw new Exception("can't load font '"~name~"' from '"~path~"'");
328 loadFont("text", textFontName);
329 loadFont("texti", textiFontName);
330 loadFont("textb", textbFontName);
331 loadFont("textz", textzFontName);
333 loadFont("mono", monoFontName);
334 loadFont("monoi", monoiFontName);
335 loadFont("monob", monobFontName);
336 loadFont("monoz", monozFontName);
340 // ////////////////////////////////////////////////////////////////////////// //
341 // messages
342 struct ReformatWork {
343 shared(BookText) booktext;
344 string bookFileName;
345 int w, h;
348 struct ReformatWorkComplete {
349 int w, h;
350 shared(BookText) booktext;
351 shared(LayText) laytext;
354 struct QuitWork {
358 // ////////////////////////////////////////////////////////////////////////// //
359 void reformatThreadFn (Tid ownerTid) {
360 bool doQuit = false;
361 loadFmtFonts();
362 BookText book;
363 string newFileName;
364 int newW = -1, newH = -1;
365 while (!doQuit) {
366 book = null;
367 newFileName = null;
368 newW = newH = -1;
369 receive(
370 (ReformatWork w) {
371 writeln("reformat request received...");
372 book = cast(BookText)w.booktext;
373 newFileName = w.bookFileName;
374 newW = w.w;
375 newH = w.h;
376 if (newW < 1) newW = 1;
377 if (newH < 1) newH = 1;
379 (QuitWork w) {
380 doQuit = true;
383 if (!doQuit && newW > 0 && newH > 0) {
384 try {
385 if (book is null) {
386 //writeln("loading new book: '", newFileName, "'");
387 book = loadBook(newFileName);
390 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
391 if (maxWidth < 64) maxWidth = 64;
393 // layout text
394 //writeln("layouting...");
395 auto stt = MonoTime.currTime;
396 auto lay = new LayText(laf, maxWidth);
398 lay.size = fsizeText;
399 lay.normJustify = optJustify;
401 book.formatBook(lay);
403 writeln("layouted in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds");
404 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay);
405 send(ownerTid, res);
406 } catch (Throwable e) {
407 // here, we are dead and fucked (the exact order doesn't matter)
408 import core.stdc.stdlib : abort;
409 import core.stdc.stdio : fprintf, stderr;
410 import core.memory : GC;
411 import core.thread : thread_suspendAll;
412 GC.disable(); // yeah
413 thread_suspendAll(); // stop right here, you criminal scum!
414 auto s = e.toString();
415 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
416 abort(); // die, you bitch!
420 send(ownerTid, QuitWork());