added textual layout dumper
[xreader.git] / xreaderfmt.d
blob70cc0f21e67a24f46cbd3001bf6823d19dfe8765
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;
24 import iv.strex;
26 import arsd.simpledisplay;
27 import arsd.color;
28 import arsd.png;
29 import arsd.jpeg;
31 import iv.nanovg;
32 import iv.nanovg.oui.blendish;
33 import iv.vfs;
34 import iv.vfs.io;
36 import xmodel;
37 import xiniz;
39 import booktext;
41 import xreadercfg;
42 import xlayouter;
45 // ////////////////////////////////////////////////////////////////////////// //
46 BookText loadBook (string fname) {
47 __gshared bool eliteShipsLoaded = false;
49 import std.path;
50 //import core.memory : GC;
51 fname = fname.expandTilde.absolutePath;
52 //writeln("loading '", fname, "'...");
53 //GC.disable();
54 auto stt = MonoTime.currTime;
55 auto book = new BookText(fname);
56 //GC.enable();
57 //GC.collect();
58 { import std.stdio; writeln("loaded: '", fname, "' in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
60 string[] lines;
61 string[] comments;
62 try {
63 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
64 if (line != fname) {
65 lines ~= line;
66 } else {
67 while (lines.length && lines[$-1].isComment) {
68 comments ~= lines[$-1];
69 lines = lines[0..$-1];
73 } catch (Exception) {}
74 try { import std.file; mkdirRecurse(RcDir); } catch (Exception) {}
75 auto fo = VFile(buildPath(RcDir, ".lastfile"), "w");
76 foreach (string s; lines) fo.writeln(s);
77 foreach_reverse (string s; comments) fo.writeln(s);
78 fo.writeln(fname);
80 stateFileName = buildPath(RcDir, fname.baseName~".rc");
82 if (!eliteShipsLoaded) {
83 loadEliteShips();
84 eliteShipsLoaded = true;
87 return book;
91 // ////////////////////////////////////////////////////////////////////////// //
92 public void formatBook (BookText book, LayText lay) {
93 assert(book !is null);
95 void dumpTree (Tag ct) {
96 while (ct !is null) {
97 { import std.stdio; writeln("tag: ", ct.name); }
98 ct = ct.parent;
102 void putParaContentInternal (Tag ct) {
103 if (ct is null) return;
104 switch (ct.name) {
105 case "strong": lay.put(LayText.BoldOnCh); break;
106 case "emphasis": lay.put(LayText.ItalicOnCh); break;
107 case "image": return;
108 case "style": return;
109 case "a": break;
110 //case "p": break;
111 //case "v": break;
112 case "": lay.put(ct.text); return;
113 default: dumpTree(ct); throw new Exception("invalid content tag: '"~ct.name~"'");
115 foreach (Tag tag; ct.children) putParaContentInternal(tag);
116 switch (ct.name) {
117 case "strong": lay.put(LayText.BoldOnCh); break;
118 case "emphasis": lay.put(LayText.ItalicOnCh); break;
119 default:
123 void putTagContents (Tag ct) {
124 if (ct is null) return;
125 foreach (Tag tag; ct.children) putParaContentInternal(tag);
126 lay.put(LayText.EndParaCh);
129 void putParas (Tag ct) {
130 foreach (Tag tag; ct.children) {
131 if (tag.name.length == 0) continue;
132 if (tag.name == "p") {
133 putTagContents(tag);
134 } else if (tag.name == "empty-line") {
135 lay.put(LayText.EndLineCh);
136 } else if (tag.name == "text-author") {
137 } else {
138 dumpTree(ct);
139 throw new Exception("invalid content tag: '"~tag.name~"'");
144 void putAuthor (Tag ct) {
145 foreach (Tag tag; ct.children) {
146 if (tag.name.length == 0) continue;
147 if (tag.name == "text-author") {
148 lay.put(LayText.EndParaCh);
149 putTagContents(tag);
150 lay.put(LayText.EndParaCh);
155 void putCite (Tag ct) {
156 lay.put(LayText.CiteCh);
157 putParas(ct);
158 putAuthor(ct);
159 lay.put(LayText.NormalTextCh);
162 void putEpigraph (Tag ct) {
163 lay.put(LayText.EpigraphCh);
164 putParas(ct);
165 putAuthor(ct);
166 lay.put(LayText.NormalTextCh);
169 void putStanza (Tag ct) {
170 foreach (Tag tag; ct.children) {
171 if (tag.name.length == 0) continue;
172 if (tag.name == "text-author") continue;
173 if (tag.name == "title") { dumpTree(ct); throw new Exception("titles in stanzas are not supported yet"); }
174 if (tag.name == "subtitle") { dumpTree(ct); throw new Exception("subtitles in stanzas are not supported yet"); }
175 if (tag.name == "epigraph") { dumpTree(ct); throw new Exception("epigraphs in poems are not supported yet"); }
176 if (tag.name == "date") continue;
177 if (tag.name == "v") { putTagContents(tag); continue; }
178 dumpTree(ct);
179 throw new Exception("invalid content tag: '"~tag.name~"'");
183 void putPoem (Tag ct) {
184 lay.put(LayText.PoemCh);
185 // put epigraph (not yet)
186 // put title and subtitle
187 foreach (Tag tag; ct.children) {
188 if (tag.name == "title") {
189 lay.size = lay.size+4;
190 scope(exit) lay.size = lay.size-4;
191 putParas(tag);
192 lay.put(LayText.EndParaCh);
193 } else if (tag.name == "subtitle") {
194 lay.size = lay.size+2;
195 scope(exit) lay.size = lay.size-2;
196 putParas(tag);
197 lay.put(LayText.EndParaCh);
200 foreach (Tag tag; ct.children) {
201 if (tag.name.length == 0) continue;
202 if (tag.name == "text-author") continue;
203 if (tag.name == "title") continue;
204 if (tag.name == "subtitle") continue;
205 if (tag.name == "epigraph") { dumpTree(ct); throw new Exception("epigraphs in poems are not supported yet"); }
206 if (tag.name == "date") continue;
207 if (tag.name == "stanza") { putStanza(tag); lay.put(LayText.EndParaCh); continue; }
208 dumpTree(ct);
209 throw new Exception("invalid content tag: '"~tag.name~"'");
211 putAuthor(ct);
212 lay.put(LayText.NormalTextCh);
215 void putSection (Tag sc) {
216 bool sectionRegistered = false;
218 void registerSection (Tag tag) {
219 import std.conv : to;
220 if (sectionRegistered) return;
221 sectionRegistered = true;
222 string text = tag.textContent.xstrip;
223 lay.sections ~= lay.curWordIndex;
224 string name;
225 while (text.length) {
226 char ch = text.ptr[0];
227 text = text[1..$];
228 if (ch <= ' ' || ch == 127) {
229 if (name.length == 0) continue;
230 if (ch != '\n') ch = ' ';
231 if (ch == '\n') {
232 if (name[$-1] > ' ') name ~= "\n...";
233 text = text.xstrip;
234 } else {
235 if (name[$-1] > ' ') name ~= ' ';
237 } else {
238 name ~= ch;
241 name = xstrip(name);
242 if (name.length == 0) name = "* * *";
243 lay.sectionNames ~= name.to!dstring;
246 foreach (Tag tag; sc.children) {
247 if (tag.name.length == 0) continue;
248 if (tag.name == "title") {
249 lay.size = lay.size+4;
250 scope(exit) lay.size = lay.size-4;
251 lay.put(LayText.TitleCh);
252 registerSection(tag);
253 putParas(tag);
254 lay.put(LayText.NormalTextCh);
255 } else if (tag.name == "subtitle") {
256 lay.size = lay.size+2;
257 scope(exit) lay.size = lay.size-2;
258 lay.put(LayText.SubtitleCh);
259 registerSection(tag);
260 putParas(tag);
261 lay.put(LayText.NormalTextCh);
262 } else if (tag.name == "epigraph") {
263 putEpigraph(tag);
264 } else if (tag.name == "section") {
265 lay.put(LayText.EndParaCh);
266 putSection(tag);
267 } else if (tag.name == "p") {
268 putTagContents(tag);
269 } else if (tag.name == "empty-line") {
270 lay.put(LayText.EndParaCh);
271 } else if (tag.name == "cite") {
272 putCite(tag);
273 } else if (tag.name == "table") {
274 lay.put(LayText.CiteCh);
275 lay.put("TABLE SKIPPED");
276 lay.put(LayText.NormalTextCh);
277 } else if (tag.name == "poem") {
278 putPoem(tag);
279 } else if (tag.name == "image") {
280 } else if (tag.name == "style") {
281 } else {
282 dumpTree(tag);
283 throw new Exception("invalid tag in section: '"~tag.name~"'");
288 foreach (Tag tag; book.content.children) {
289 if (tag.name == "title") {
290 lay.size = lay.size+4;
291 scope(exit) lay.size = lay.size-4;
292 lay.put(LayText.EndParaCh);
293 lay.put(LayText.EndParaCh);
294 lay.put(LayText.TitleCh);
295 putParas(tag);
296 lay.put(LayText.NormalTextCh);
297 } else if (tag.name == "subtitle") {
298 lay.size = lay.size+2;
299 scope(exit) lay.size = lay.size-2;
300 lay.put(LayText.SubtitleCh);
301 putParas(tag);
302 lay.put(LayText.NormalTextCh);
303 } else if (tag.name == "epigraph") {
304 putEpigraph(tag);
305 } else if (tag.name == "section") {
306 putSection(tag);
310 lay.finalize();
314 // ////////////////////////////////////////////////////////////////////////// //
315 private __gshared LayoutFonts laf; // layouter font stash
318 // ////////////////////////////////////////////////////////////////////////// //
319 private void loadFmtFonts () {
320 laf = new LayoutFonts();
322 void loadFont (string name, string path) {
323 int fid = laf.fs.fonsAddFont(name, path);
324 if (fid < 0) throw new Exception("can't load font '"~name~"' from '"~path~"'");
327 loadFont("text", textFontName);
328 loadFont("texti", textiFontName);
329 loadFont("textb", textbFontName);
330 loadFont("textz", textzFontName);
332 loadFont("mono", monoFontName);
333 loadFont("monoi", monoiFontName);
334 loadFont("monob", monobFontName);
335 loadFont("monoz", monozFontName);
339 // ////////////////////////////////////////////////////////////////////////// //
340 // messages
341 struct ReformatWork {
342 shared(BookText) booktext;
343 string bookFileName;
344 int w, h;
347 struct ReformatWorkComplete {
348 int w, h;
349 shared(BookText) booktext;
350 shared(LayText) laytext;
353 struct QuitWork {
357 // ////////////////////////////////////////////////////////////////////////// //
358 void reformatThreadFn (Tid ownerTid) {
359 bool doQuit = false;
360 loadFmtFonts();
361 BookText book;
362 string newFileName;
363 int newW = -1, newH = -1;
364 while (!doQuit) {
365 book = null;
366 newFileName = null;
367 newW = newH = -1;
368 receive(
369 (ReformatWork w) {
370 //{ import std.stdio; writeln("reformat request received..."); }
371 book = cast(BookText)w.booktext;
372 newFileName = w.bookFileName;
373 newW = w.w;
374 newH = w.h;
375 if (newW < 1) newW = 1;
376 if (newH < 1) newH = 1;
378 (QuitWork w) {
379 doQuit = true;
382 if (!doQuit && newW > 0 && newH > 0) {
383 try {
384 if (book is null) {
385 //writeln("loading new book: '", newFileName, "'");
386 book = loadBook(newFileName);
389 int maxWidth = newW-4-2-BND_SCROLLBAR_WIDTH-2;
390 if (maxWidth < 64) maxWidth = 64;
392 // layout text
393 //writeln("layouting...");
394 auto stt = MonoTime.currTime;
395 auto lay = new LayText(laf, maxWidth);
397 lay.size = fsizeText;
398 lay.normJustify = optJustify;
400 book.formatBook(lay);
402 { import std.stdio; writeln("layouted in ", (MonoTime.currTime-stt).total!"msecs", " milliseconds"); }
403 auto res = ReformatWorkComplete(newW, newH, cast(shared)book, cast(shared)lay);
404 send(ownerTid, res);
405 } catch (Throwable e) {
406 // here, we are dead and fucked (the exact order doesn't matter)
407 import core.stdc.stdlib : abort;
408 import core.stdc.stdio : fprintf, stderr;
409 import core.memory : GC;
410 import core.thread : thread_suspendAll;
411 GC.disable(); // yeah
412 thread_suspendAll(); // stop right here, you criminal scum!
413 auto s = e.toString();
414 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
415 abort(); // die, you bitch!
419 send(ownerTid, QuitWork());